TECH PLAY

アプトポッド

アプトポッド の技術ブログ

248

はじめに 製品開発グループの野本です。 組込ソフトウェアエンジニアとしてデータ収集用端末のソフトウェア開発を担当しています。 今回はROSの取り組みの一環として、C++で任意のトピックをPublish/Subscribeする方法についてご紹介します。 はじめに 背景 調査結果 性能測定 topic_tools::ShapeShifterとは? (参考) rosbagはどうやって任意のトピックをSubscribeしているのか? JSON変換もしたい場合 まとめ 背景 ROSは複数のノードが トピック を介してノード間通信を行っています。 ROSノードとROS以外のアプリケーションが通信する選択肢として、 rosbridge があります。 rosbridgeはWebSocket、TCP、UDPに対応 *1 しており、TCPを使用する場合はrosbridge_tcpノードがTCPサーバーとして動作します。クライアントは指定したトピックのデータをJSONとしてPublish/Subscribeすることができますが、以下のような懸念点があります。 JSON化するとデータが肥大化しやすい、データをそのまま扱う場合は変換処理が冗長(→生データのまま扱いたい...) pythonで動いておりパフォーマンスが不安(→C++で動かしたい...) これらの懸念点を解決するため、以下のようにC++ノードで動作し、JSONではなくバイナリの生データで任意トピックのPublish/Subscribeをする方法について調査しました。 調査結果 データ型に topic_tools::ShapeShifter を利用することで、C++で任意トピックを生データのままPublish/Subscribeすることができました。具体的には以下のように、ShapeShifter型のPublisherとSubscriberを利用します。 Subscribe // コールバック関数 void topicCallback( const ShapeShifter::ConstPtr& topic_msg) { // トピック情報の取得 const std::string& md5sum = topic_msg->getMD5Sum(); const std::string& datatype = topic_msg->getDataType(); const std::string& definition = topic_msg->getMessageDefinition(); const uint32_t topic_msg_size = topic_msg->size(); // 生データ(バイナリ)の取得 std::vector< uint8_t > data; data.resize(topic_msg_size); ros::serialization::OStream stream(data.data(), topic_msg_size); topic_msg->write(stream); // 取り出したデータをプロセス間通信等でROS空間外に送信する .... } int main( int argc, char ** argv) { ... const std::string topic_name = "/topic_name" ; ros::Subscriber sub = nh.subscribe< const topic_tools::ShapeShifter::ConstPtr&>(topic_name, 10 , topicCallback); ... } 参考: How to create a generic Topic Subscriber Advertise, Publish int main( int argc, char ** argv) { ... // ShapeShifter設定 std::string topic_name; std::string datatype; std::string md5sum; std::string definition; // 上記stringをPublishしたい情報に合わせて設定する(記述省略) // Subscribe側で取得したデータを設定するとそのままPublishできる topic_tools::ShapeShifter shape_shifter; shape_shifter.morph(md5sum, datatype, definition, "" ); ros::Publisher pub = shape_shifter.advertise(nh, topic_name, 100 ); // データ設定 uint8_t * data; uint32_t data_size; // data, data_sizeを設定する(記述省略) ros::serialization::OStream stream(data, data_size); shape_shifter.read(stream); pub.publish(shape_shifter); ... } 参考: Generic ROS publisher using ShapeShifter Subscribe側で取得したデータ(md5sum、datatype、definition、data)をプロセス間通信でROS空間から取り出し、ネットワークで遠隔のロボットに転送してPublishする、といったことがC++ノード&生バイナリデータで実現可能です。 性能測定 測定環境 PC: ThinkPad X1 Carbon (7th Gen) ROS/OS: Melodic / Ubuntu 18.04 データサイズ rosbridgeと今回の手法で、 こちら の動画データ(640x360、1280x720)をPublishした際の1メッセージのデータサイズを比較しました。 640x360 手法 1メッセージのデータサイズ(Byte) rosbridge 921857 今回の手法 693372 1280x720 手法 1メッセージのデータサイズ(Byte) rosbridge 3686658 今回の手法 2766972 rosbridgeと比較して 約25%のデータ量を削減 できていることがわかりました。 今回の手法では、トピック名およびSubscribe側で取得したデータ全て(md5sum、datatype、definition、data)を転送しています。md5sum、datatype、definitionはAdvertiseする際に必要なデータで、一度Advertiseした後は不要です。これらを省くことでさらにデータ量を削減できそうです。 メッセージ到達時間 rosbridgeと今回の手法で上記と同じ動画データ(1280x720)を15秒程度Publishし、受信側アプリケーションでのメッセージ到達時間 *2 を比較しました。 データの読み込み方法は、rosbridgeではソケット、今回の手法ではパイプを利用しました。 手法 到達時間 [sec] rosbridge 22353.21254657 今回の手法 22353.30257833 rosbridgeと比較して 約100msメッセージの到達時間が早い 結果となりました。 C++ノードではプロセス間でのメモリコピーを防ぎ高速化する nodelet が使用できます。今回はnodeletを使っていないため、nodeletを使うことでさらに速度が早くなりそうです。 なお、 rosbridgeはJSONデータ受信後にJSONのパース処理が必要なため、実際はこの結果以上に処理時間が必要 となります。 topic_tools::ShapeShifterとは? topic_tools::ShapeShifter は、rosbag *3 でトピックを記録(record)する際に使われているデータ型です。 rosbagが任意のトピックをSubscribeできるのはこのデータ型のおかげです。 (参考) rosbagはどうやって任意のトピックをSubscribeしているのか? 通常のROSプログラムと同じように ROS::Subscriber を使ってトピックをSubscribeしています。 rosbagのSubscribe設定について ros::Subscriber sub = n.subscribe( "chatter" , 1000 , chatterCallback); ROSチュートリアルでは上記のように、引数でオプションを設定する 使い方 をしていますが、rosbagではこのような方法ではなく、 ros::SubscribeOptions にオプションを設定して引数で渡す こちら の方法を使っています。 具体的には Recorder::subscribe() でSubscriberを初期化しており、 helper の部分を見ると doQueue() をコールバック関数として設定していることがわかります。 ros::SubscribeOptions ops; ops.topic = topic; ops.queue_size = 100 ; ops.md5sum = ros::message_traits::md5sum<topic_tools::ShapeShifter>(); ops.datatype = ros::message_traits::datatype<topic_tools::ShapeShifter>(); ops.helper = boost::make_shared<ros::SubscriptionCallbackHelperT< const ros::MessageEvent<topic_tools::ShapeShifter const > &> >( boost::bind(&Recorder::doQueue, this , _1, topic, sub, count)); <-- doQueue()をコールバックに設定 ops.transport_hints = options_.transport_hints; *sub = nh.subscribe(ops); 上記設定や doQueue() の第一引数を見ると、 topic_tools::ShapeShifter という型でやり取りしていることがわかります。 void Recorder::doQueue( const ros::MessageEvent<topic_tools::ShapeShifter const >& msg_event, string const & topic, shared_ptr<ros::Subscriber> subscriber, shared_ptr< int > count) JSON変換もしたい場合 ShapeShifterのコールバック関数内で渡されたデータをJSONに変換したい場合、 ros_type_introspection を使うことで変換することができます。 ※ 現在、Noeticではros_type_introspectionがサポートされていないようで、後継の ros_msg_parser を使う必要があります。 まとめ 今回は、C++で任意のトピックをPublish/Subscribeする方法についてご紹介しました。 ShapeShifterに関する日本語の情報はあまりありませんでしたので、ROS開発の一助となれば幸いです。 *1 : rosbridge_serverのlaunchファイル *2 : システム起動時からの経過時間(MONOTONIC_RAW)を使って計測 *3 : rosbag : 指定したトピックのデータをファイルに記録することや、ファイルに保存したトピックのデータを再生することができるツール。
アバター
Webチームの蔵下です。先日、弊社デザイナーの高森が公開した記事「 コンポーネントを活用したアプリケーション群のデザイン 」で紹介したように、aptpodではフロントエンドエンジニアとデザイナーとで、頻繁に議論を重ねながら開発を進めています。 開発中もコミュニケーションを取り合うことでお互いの認識齟齬は減らせるのですが、実装着手前にデザイン面で不確定要素が多いほど手戻りの手間(工数)が膨らんでしまいます。 実装着手前にすべての不確定要素を解消することは難しいですが、開発中に議論になりやすいポイントにはいくつかのパターンがありました。それを実装着手前のデザインレビューで確認できるようにチェックリストとしてまとめましたので紹介します。 チェックリスト 現在もブラッシュアップ中のチェックリストです。アプリケーションごとにチェック項目は変わりますが、議論になりやすい項目を中心に解説します。 1. 画面サイズ ・最大・最小幅でも崩れないデザインになっている ・画面構成要素ごとに可変・固定幅のルールが決まっている 2. フォント ・使用するフォントのライセンスに問題がない ・OSの違いによるフォントの差異が考慮されている 3. テキスト ・最大文字数が考慮されている ・表示領域を超える文字数のテキストが入った場合の挙動が決まっている 4. UI ・ボタンのhover, active, disable状態のデザインが作成されている ・最小幅表示でもダイアログが欠けないように考慮されている ・フォームのバリデーション結果を表示する領域が考慮されている 5. 例外 ・API実行箇所のエラー表示が考慮されている ・データ件数0件の表示が考慮されている 6. アニメーション ・アニメーションを設置する場所・イメージが共有されている 1. 画面サイズ ・最大・最小幅でも崩れないデザインになっている ・画面構成要素ごとに可変・固定幅のルールが決まっている アプリケーションは表示する画面サイズによって見え方が変化します。通常の画面サイズできれいに収まっているレイアウトでも、仕様上の最小サイズだと要素が1画面で表示できずレイアウトが崩れてしまう恐れがあります。 実装着手前に、目の前のデザインが画面サイズごとにどう見えるのかをイメージすることが重要です。 画面を構成する要素ごとに、幅のルール *1 や、表示のルール *2 をデザイナーと事前にすり合わせておきましょう。 Github - React のページを画面サイズを切り替えて表示した様子 2. フォント ・使用するフォントのライセンスに問題がない ・OSの違いによるフォントの差異が考慮されている ネット上にはWebフォントの技術的な話題は多いですが、ライセンスについて言及されている情報はあまりありません。フォントファイルをそのままサーバーへ設置して配信することは問題がありそうということは想像しやすいですが、 サブセット化がフォントの改変にあたるためフォントによっては規約違反 であることは見落とされがちです。デザインで使用されているフォントの実装方法がライセンス的に問題がないか *3 、念のためデザイナーに確認しましょう。 アプリケーションによっては、OSにプリインストールされているフォントでデザインを構成する場合もあります。デザイン作成作業はmacOSで行われることが多いため、Windowsで適用されるフォントでもデザインに問題がないかデザイナーに確認しておきましょう。 3. テキスト ・最大文字数が考慮されている ・表示領域を超える文字数のテキストが入った場合の挙動が決まっている アプリケーションの開発中は、領域内に収まる理想的なダミーテキストで実装を進めがちです。そのままの状態で実装を進めると、重要な情報なのに途中でテキストが欠けてしまう、文字数が多くなってしまい想定外の折返しが発生するなどの不整合に気づくのが遅れてしまいます。 仕様策定の段階で最大文字数をクライアントとすり合わせ、どうしても運用してみないと判断できないという箇所は、想定する最大文字数を超えたときの挙動も検討しておきましょう。 3点リーダー で省略しても問題ない情報なのか、問題がある場合は ツールチップ などで全文表示できるようにするといったところまで検討できると安心です。 文字数の多いテキストは3点リーダーで省略 4. UI ・ボタンのhover, active, disable状態のデザインが作成されている ・最小幅表示でもダイアログが欠けないように考慮されている ・フォームのバリデーション結果を表示する領域が考慮されている UIは、ユーザーアクションやAPIのレスポンスによって影響を受けるため、デザイン作成時点で見えづらい部分が多いです。使用するUIの種類によっても確認ポイントが変わってくるため、デザイナーと認識を合わせておきましょう。 5. 例外 ・API実行箇所のエラー表示が考慮されている ・データ件数0件の表示が考慮されている 例外のパターンはデザイナーから見えづらい部分のため、フロントエンドエンジニアの方で事前にリストアップしておくと、デザイン作成の漏れを減らせます。例外とは少しニュアンスが違うかもしれませんが意外と見落としがちなのが、データ件数が0件の表示です。実際の運用に入ると目に触れる機会も減るため不要に感じますが、検索結果の0件表示などと共通化して用意しておくことで、アプリケーションのUXが向上します。 Gmailでのデータ0件表示 6. アニメーション ・アニメーションを設置する場所・イメージが共有されている アニメーションは、アプリケーションのUXに大きな影響を与えます。過度なアニメーションはユーザーへ不快感を与え、適切なアニメーションはユーザーの行動をサポートし安心感を与えます。 アニメーションにはさまざまな手法があり正解はないのですが、私は普段から 統一感 を意識しています。無理に複雑なアニメーションを多用するのではなく、シンプルなアニメーションをアプリケーション全体で統一することで、ユーザーも無意識に規則性を認識でき、わかりやすさにつながります。 アニメーションの解説だけでも記事が1本書けそうなので本記事では深堀りしませんが、シンプルなアニメーションだけで心地よさを伝えられるコツを一部紹介します。 使用するeasing, 秒数は用途によって統一する easingに関しては linear + もう1種類くらいが喧嘩しない 細かなeasingの確認は イージング関数チートシート がおすすめ 数あるeasingの中で緩急をいい感じに出しやすい Expo がおすすめ inとoutで同じ秒数・easingにこだわらない inはユーザーへのわかりやすさのため、outは次の操作への邪魔にならないようにするため よくやる組み合わせ(秒数は例) in(MouseOver)は0.5sで Expo out(MouseOut)は0.2sで linear イージング関数チートシート - easeInExpo でeasingの動作を確認している様子 まとめ フロントエンドエンジニア目線でのデザインチェックリストを紹介しました。本記事で一番伝えたかったことは、 一つの要件をフロントエンドエンジニアとデザイナーの異なる目線で見ることで、一人では気づけなかったことを互いに補いあえる ということです。実装に入る前にお互いに意見を出し合うことで、実装が始まっても納得感を持って突っ走ることができます。 今後もさまざまなアプリケーションへ実戦投入し、チェックリストをブラッシュアップしていければと思います! *1 : 画面サイズによって幅が伸縮する可変幅、画面幅が変化しない固定幅のどちらにするかを決めます。 *2 : 画面サイズによって表示・非表示を切り替えるのか、表示させる場合はどう表示するか(最小サイズ時はナビゲーションを畳んで開閉できるようにするなど)のルールを決めます。 *3 : フォントのライセンスについてはこちらの記事「 フォントのライセンスまとめ 」で詳しく紹介されているので参照ください。
アバター
aptpodデザインチームの高森です。ウェブアプリケーションのデザインをメインにスマートフォンアプリケーションや印刷物のデザインを担当しております。現在aptpodでは、 intdash のサービスを利用するための周辺アプリケーションを開発中です。関連するアプリケーションを複数デザインする中で、デザイン共通化に向けた取り組みや、デザインで検討した点をまとめてみました。 背景 intdashには具体的に、以下のようなユーティリティアプリケーションがあります。 アカウント設定ツール エッジ一覧・確認ツール 計測管理ツール エッジ管理ツール(管理者向け) など intdashを使って計測の管理・閲覧・解析をするためには、データがきちんと取れているかの確認や、使用中のアカウントの把握などが必要になります。その設定や管理に使用されるのがユーティリティアプリケーションです。メンテナンス性を考慮して、これら機能はそれぞれ個別のアプリケーションとして開発しています。 開発中アプリケーションの画面 コンポーネントの活用 開発コストの削減、デザインの統一感を図るため、共通化できる部分はコンポーネント化しました。 (ヘッダー・サイドメニューの基本レイアウト、ボタン・フォームなどのパーツ部分を共通可) どこに何の情報を配置するかは事前にエンジニアと相談しながら決めておくと、機能の追加や更新が必要となった際に影響範囲をまとめることができ、他のデザイナーへの共有も漏れなくできました。 現時点ではデザインシステムとしてまではまだ運用できていませんが、将来的にパーツをアプリケーション間でそのまま使い回せる様なワークフローも目指しています。 デザイン作業しやすいデザインファイルにしておくと良し◎ コンポーネントはなるべく汎用的で簡潔にまとめたいところなのですが、デザイナーの実作業としては、レビューやマニュアル・ウェブサイトに掲載するサンプルページの作成などもあり、画面の色々な状態が作成しやすくなっていることも重要です。コンポーネント管理の観点とは別に、実際に作業しやすいファイルにまとまっていると使いやすいなと感じました。 複数人での共有を想定すると、コンポーネントの精度をどこまで設定するかについては、デザインのワークフローを確認しながらチーム内で繰り返しアップデートが必要そうです。 アプリケーションアイコン aptpodのアプリケーションは黒をベースにデザインしています。(aptpodのデザインについては こちら ) 色のトーンやフォントを共通化し、aptpodテイストを踏襲することで統一感を出せる反面、ぱっとみた時にアプリケーションを区別しにくいという課題が上がりました。他社ではアプリケーションごとにキーカラーを分けていたり、アイコンのみ色分けしているものがありました。intdashのように開発が活発に行われているアプリケーションに同じルールを適用しようとすると、アプリケーションが増えるごとに色数が増え、周辺アプリケーションと整合性が取れなくなる恐れがあったため、ルールとして採用することはは見送りました。代わりに、ヘッダー部分に表示されるアプリケーションアイコンに1秒未満のマイクロアニメーションを付けることによってそれぞれ特徴を持たせることにしました。 ブックマーク登録からファビコンでアプリケーションを探すことを想定し、形状はなるべくシンプルに、モチーフの組み合わせは使わず表現するようにしています。 アプリケーションごとに事前に確認しておくべきデザイン検討項目 デザイン検討時に早めに確認しておくと良い点や、作成した画面デザインをエンジニアへ展開する際に、事前に確認しておくとコミュニケーションがスムーズに進むポイントがあったので共有します。 ページングをどうするか aptpodのアプリケーションでは表示するデータが大量になるものが多いです。1度に数万件を表示するケースも想定されるため、1ページの表示件数やスクロールの振る舞い(どの位置からスクロールさせる?など)を事前に確認しておくことで、レイアウトの検討に役立ちます。 データがない時、テキストがはみ出る時の表示 理想的な見え方のみでデザインを検討せず、特にリスト表示画面を検討する時は、データが0、10(サンプル画として理想的な数)、1000個の時をセットにして見え方を確認するようにしています。 コントラストは大丈夫か 全てのモニター設定をケアする必要はありませんが、自分の環境以外のモニターで確認してみる、ウェブ基準を確認するなどしてある程度の視認性は担保するように注意しています。 ウィンドウサイズ ウィンドウ幅に応じたブレークポイントを指定してはいますが、実際の動きを見ると意外とすぐに画面幅が畳まれてしまうんだな...と思うことも多いです。デザイナーは特に大きなディスプレイと高解像度で作業していることが多いと思いますが、実際のユーザーはオフィスや出張先などでノートPCを利用する場合が多いと想定した上で、基準の解像度を1280px程度に設定するようにしました。 アラート表示 常に表示されるものではないのですが、表示が必須かつ実装段階で追加になる場合も多いので、事前に各情報に対してエラーが必要かどうかを確認しておくとスペースを確保しやすいです。(こいつがなければすっきり収まるのに...と毎回とても悩まされてます) 上記に加え、実際の利用画面をイメージしやすくするために、テキストはLorem ipsumや"サンプルサンプル...."といったダミーテキストではなく、なるべく実際の動きがイメージできるテキストを入れるようにしています。 例えば日付欄も同じ数字の繰り返しを入れがちでしたが、正しく入れ込むと「日付の違いが分かりにくいのでは?」「時間が大きく表示されていた方が良いかも?」といった気付きがあったりします。 左:ダミーテキストを入れたもの、右:実装に近いテキストを入れたもの 第三者にレビューする場合や、デザインを引き継ぐ際にも、実際に近いテキストが入っていると内容を理解してもらいやすいため、レイアウト検討できるうちに日付やテキストは本物を想定した内容を入れておくと良いと感じました。(実装前にアプリケーションの実際の振る舞いを完璧に想定することはなかなか難しいのですが、開発が進む中で実装中のアプリケーションを確認できることは大変助かっています。) おわりに 今回は、複数のアプリケーションをデザインする中で得たTipsや効率化に向けての取り組みをご紹介しました。 現在は開発を進めつつデザインをまとめつつ、という段階でコンポーネントを活用したワークフローについてはまだ模索中ではありますが、変更履歴の残し方や命名規則など実運用する際の課題も見えてきたところです。実際のデザイン作業にも取り入れやすいコンポーネントを引き続き検討し、効率よく必要な検討にしっかり時間を取れるデザインワークフローを確立させていきたいです。
アバター
アプトポッドにて、テクニカルライターとして製品マニュアルの制作を担当している篠崎です。 現在弊社では、製品マニュアルの制作に、 Sphinx を導入しようとしています。Sphinxは、1つの原稿ファイルからHTML、PDF等を出力できるドキュメントジェネレーターです。この記事では、SphinxにLuaLaTeXを組み合わせて日本語PDFを生成する方法を探ってみました。 背景 SphinxでLuaLaTeXを使う設定 (A) LuaLaTeXを使用する (B) LaTeXドキュメントクラスとしてltjsbookを使用する (C) Polyglossiaパッケージを読み込まないようにする (D) サンセリフ系フォント、ゴシック系フォントを指定する(AXISフォントを使用するため) (E) デフォルトのフォントをサンセリフ系、ゴシック系に変更する(AXISフォントを使用するため) おわりに、今後に向けて 参考文献 背景 弊社では以前から、マニュアル制作にソフトウェア開発の手法を取り入れつつ、作業の効率化に取り組んできました。その様子については、以下のような記事でご紹介したことがあります。 製品マニュアルの制作に開発手法を取り入れた話(aptpod Advent Calendar 2018) エンジニアによるユーザーマニュアルの作り方(aptpod Advent Calendar 2019) いずれも大変効果的な手法だったのですが、最近、マニュアルの内容を継続的にアップデートしていく上で、いくつかの課題が生じており、他の方法の検討も進めています。 そんななか、候補としてSphinxを使ってみることにしました。Sphinxは、reStructuredText記法で書かれた原稿を、HTMLウェブページやPDF、ePubなどに変換してくれるドキュメント生成ツールです。最初からウェブページとPDFの両方に対応しているというのが大きな魅力です。(注:ウェブページを作成するための静的サイトジェネレーターは世の中にたくさんありますが、PDF出力には対応していない場合がほとんどです。好みの静的サイトジェネレーターに別のPDF作成ツールを組み合わせるのは魅力的な選択肢ですが、それはそれで大仕事になります。) Sphinxでは、PDF出力はLaTeX経由で行われます。reStructuredText記法で原稿を書いてPDF作成コマンドを実行すると、Sphinxにより原稿がLaTeXファイルに変換され、その後、LaTeXエンジンによりPDFに変換されます。なお、LaTeXエンジンはSphinxには含まれませんので、別途インストールする必要があります。 TeX Live を使ってインストールするのが便利です。 Sphinxを使って、reStructuredTextからPDFを作成する Sphinxを試し始めたところ、弊社の製品マニュアル用としてはいくつかの課題が浮かび上がりました。そのうち1つがフォントです。弊社では、スマートで美しい AXISフォント をウェブアプリケーションや印刷物で使用しています。 以前の記事 のようにWordでマニュアルを作成する際も、AXISフォントを使用し、PDFに埋め込んでいました。Sphinxを使う場合も、ぜひこのフォントを使用したいと考えました。Sphinxの場合、PDF出力は前述のようにLaTeX経由で行われるため、LaTeXでこのフォントを使う必要があります。しかしLaTeXでのフォントの扱いは簡単ではありません。 調べてみたところ、LaTeXエンジンの1つであるLuaLaTeXを使うと、より簡単にフォントの変更ができることが分かりました。Sphinxでの日本語PDF出力には、デフォルトではpLaTeXが使われますが、設定によりLuaLaTeXに変更することができます。以下ではLuaLaTeXを使う場合の設定例を紹介します。 (Sphinxは、設定ファイル conf.py によりさまざまな設定が可能です。また、それ以外にもカスタマイズのための入り口が多数用意されており、柔軟に変更できます。) SphinxでLuaLaTeXを使う設定 新しくSphinxのドキュメントプロジェクトを作って、最小限の設定を行うところを解説します。 使用した環境は以下の通りです。 Windows 10 Sphinx v3.0.2(※ v3.1.0~を使用すると、以下に紹介する設定ではうまくいかないことが分かっています *1 ) TeX Live 2020 まずは、 sphinx-quickstart コマンドを使ってプロジェクトディレクトリを作成します。端末で sphinx-quickstart を実行すると通常は対話モードになり、プロジェクト名などを順々に入力しなければなりませんが、 -q オプションを使うと、非対話モードで実行することができます。 sphinx-quickstart -q -p サンプルマニュアル -a 株式会社〇〇 -v 1.0 -l ja sample_manual このコマンドにより、以下のようにプロジェクトディレクトリが作成されます。 プロジェクト名( -p オプション): サンプルマニュアル 著者( -a オプション): 株式会社〇〇 プロジェクトバージョン( -v オプション): 1.0 言語( -l オプション): 日本語(ja) ディレクトリ名: sample_manual プロジェクトディレクトリの中には、原稿ファイル index.rst 、設定ファイル conf.py 、そのほか必要なサブディレクトリが作成されます。 conf.py には、プロジェクト名や著者の設定がすでに書き込まれています。以下のようになっているはずです。 (省略) # -- Project information ----------------------------------------------------- project = 'サンプルマニュアル' copyright = '2020, 株式会社〇〇' author = '株式会社〇〇' # The short X.Y version version = '1.0' # The full version, including alpha/beta/rc tags release = '1.0' # -- General configuration --------------------------------------------------- (省略) language = 'ja' (省略) ここで、 sample_manual ディレクトリに移動し、 make latexpdf コマンドを実行すると、 _build ディレクトリにPDFが出力されます。このPDFはデフォルトのpLaTeXにより出力されたものです。 LuaLaTeXを使用するためには、設定ファイル conf.py に latex_engine = 'lualatex' を追加します。しかしこれだけではエラーになってしまいます。日本語のプロジェクト( language = 'ja' )では、自動的に、LaTeXのjsbookドキュメントクラスが使用されるためです。jsbookはpLaTeXでの処理を必要とするので、これが読み込まれる段階でエラーになります。そこで、使用するドキュメントクラスを変更し、以下のように設定しました。これでコンパイルエラーなしでPDFが出力されるようになります。 併せて、当初の目標に沿って、AXISフォントを使用する設定を書き込みました(使用するフォントは、LuaLaTeXが認識する場所にインストールされている必要があります)。 # -- Options for LaTeX output ------------------------------------------------- # (A) LuaLaTeXを使用する latex_engine = 'lualatex' # (B) LaTeXドキュメントクラスとしてltjsbookを使用する latex_docclass = {'manual': 'ltjsbook'} latex_elements = { # (C) Polyglossiaパッケージを読み込まないようにする 'polyglossia': '', # (D) サンセリフ系フォント、ゴシック系フォントを指定する(AXISフォントを使用するため) 'fontpkg': r''' \usepackage[no-math,scale=1.0]{luatexja-fontspec} \setsansfont{AxisStd-Regular.otf}[ BoldFont = AxisStd-Medium.otf ] \setsansjfont{AxisStd-Regular.otf}[ BoldFont = AxisStd-Medium.otf ] ''', # (E) デフォルトのフォントをサンセリフ系、ゴシック系に変更する(AXISフォントを使用するため) 'preamble': r''' \renewcommand\familydefault{\sfdefault} \renewcommand\kanjifamilydefault{\gtdefault} ''' } ご覧のように、 latex_elements の fontpkg や preamble に指定された文字列は、LaTeXのコマンドそのものです。これらは、SphinxによってLaTeXファイルに書き込まれます。 このように設定すると、LuaLaTeXにより以下のようなPDFが出力されました。 LuaLaTeXにより生成されたPDF ポイントは以下の通りです。記号(A)~(E)は上のコード内の記号に対応しています。 (A) LuaLaTeXを使用する 前述のとおり、LaTeXエンジンとしてLuaLaTeXを使用します。 (B) LaTeXドキュメントクラスとしてltjsbookを使用する Sphinxで日本語PDFを出力するとき、標準ではjsbookドキュメントクラスが使用されますが、それに代えてltjsbookドキュメントクラスを使用します。ltjsbookは、jsbookをもとにしたLuaLaTeX対応のドキュメントクラスです。TeX Liveに含まれていますので、Tex Liveがインストールされていれば使用することができます。 (C) Polyglossiaパッケージを読み込まないようにする SphinxでLuaLaTeXを使用すると、標準ではLaTeXのPolyglossiaパッケージが読み込まれます。Polyglossiaは、LaTeXでの多言語組版を可能にするパッケージですが、今回の例で使用すると、以下のようなフォントに関する警告が多数出力されます。 Package polyglossia Warning: Asking to add empty feature to latin font(Script="CJK" to scripttag "") on input line xx. 今回はPolyglossiaパッケージを使用する必要はないので、これをオフにしています。 (D) サンセリフ系フォント、ゴシック系フォントを指定する(AXISフォントを使用するため) luatexja-fontspecパッケージを使って、フォントを指定します。 弊社の場合は、ここでAXISフォントを指定します。AXISフォントはゴシック系のフォントですので、また、和文だけでなく欧文(文中の英数字)にも使用したいので、 setsansfont と setsansjfont の両方を行います。基本書体としてAxisStd-Regular.otfを、ボールド書体としてAxisStd-Medium.otfをそれぞれ設定しました。 no-math オプションは、数式部分のフォント設定を変更しないために指定しています。 scale オプションは、欧文フォントに対する和文フォントのサイズの比率を設定するものです。和文と欧文に別々のフォントを使用する場合に、大きさのバランスを調整するのに使用します。今回は、和文と欧文ともに同じAXISフォントを使用するため、バランスを調整する必要はありませんが、指定をしないとltjsbookドキュメントクラスが持つ \Cjascale の値(0.924715)が使用されるため、 scale=1.0 を指定しています。 (E) デフォルトのフォントをサンセリフ系、ゴシック系に変更する(AXISフォントを使用するため) ltjsbookの本文のフォントは、欧文はセリフ系、和文は明朝になっています。これをサンセリフ系、ゴシック系に変更します。これにより、本文の欧文部分、和文部分ともに上記(D)で指定したAXISフォントが使用されます。 おわりに、今後に向けて 以上のように設定することで、AXISフォントを指定し、LuaLaTeXを使って日本語のPDFを出力できるようになりました。紹介した例は説明用の最小限のもので、マニュアル制作の実務では、ここからさまざまな設定や、スタイルの調整が必要になります。 また、実務でSphinxを活用するにあたっては、PDFの出力方法以外にも、ウェブページ出力のための設定、翻訳の方法など、検討しなければならないことはいくつもあります。 今回のPDF作成により、Sphinxは柔軟にカスタマイズが可能で、使いやすいツールであることが分かりました。これを効率的なマニュアル制作に活用できるよう、今後も知恵を絞りたいと思っています。 参考文献 Sphinx日本語公式サイト 主に以下のページを参照しました: 設定 、 LaTeXのカスタマイズ Sphinx-Users.jp 「pLaTeXからLuaLaTeXへの移行」に関するクイズ luatexjaパッケージのドキュメント fontspecパッケージのドキュメント *1 : Sphinx v3.1.0~では、言語設定をjaにすると、pLaTeX専用のpxjahyperパッケージが読み込まれるためです。対処方法は別途検討したいと思います。
アバター
研究開発グループの酒井 ( @ neko_suki )です。 今回は、「Turtlebot3の実機を使ったSLAMとNavigationをAWS RoboMaker上でやってみた」という取り組みについて紹介します。 SLAMは、Simultaneous Localization and Mappingの略で、ロボットによる自己位置推定と地図作成を同時に行うことです。Navigationは、指定した目的地までロボットを移動させることです。 まずは、動画をご覧ください。 www.youtube.com 動画ではAWS RoboMakerのシミュレーションジョブ上で、Turtlebot3のNavigationを行うためのノードを起動します。 そして、シミュレーション上のrvizで目的地を設定します。 そうすると、Turtlebot3が自律走行していることが確認できます。 今回の記事では、この動作をどのように実現したのかをお伝えします。 なお、本内容は、 ROS Japan UG #37 オンラインROS勉強会 で発表した内容をベースに執筆しております。 資料は SlideShare にアップロードしてあります。 取り組みのモチベーション 技術的に検討をしたポイント ①インターネット経由でSLAM/Navigationを動かすために必要なROSトピックを調べる SLAM に必要なROSトピックの確認 turtlebot3_slam_gmapping がpublish/subscribeしているROSトピックの一覧 Navigationに必要なROSトピックの確認 Navigationに使用されているROSトピックの一覧 帯域の計測 ②AWS RoboMaker上でSLAM/Navigationを動かす。 ROS bag再生に対応したシミュレーションアプリケーションを作る ROS bag再生用のシミュレーションジョブの作成 まとめ 取り組みのモチベーション 過去に弊社製品のintdash Edge と AWS RoboMaker を用いた遠隔制御の取り組みについてご紹介しました。 参考 tech.aptpod.co.jp tech.aptpod.co.jp 現在でも研究開発グループでは、AWS RoboMakerとの連携による弊社製品のユースケースの拡充を検討しています。 今回の取り組みではその一環として、SLAMやNavigationのような負荷の高い処理をAWS RoboMaker上で実行するための検証を行いました。 従来は、SLAMやNavigationなどの処理はローカルネットワーク上で、Turtlebot3の実機と接続したPC上で実行していました。 従来の構成 今回の取り組みでは、このPC上で行っていた処理をAWS RoboMaker上で行えるようにします。 弊社製品のintdash Edge は ROSトピックをインターネット経由で流すことが出来ます。なので、SLAMやNavigationに必要なROSトピックをTurtlebot3の実機からAWS RoboMaker上に流したり、逆にAWS RoboMaker上で発行されたNavigation用のROSトピックをTurtlebot3の実機に流すことも可能になります。 AWS RoboMakerのシミュレーションジョブではrvizをサポートしています。AWS RoboMakerのシミュレーションジョブは、Gazeboを用いた物理シミュレーションやROS bagの再生などのシミュレーションに用いるものですが、今回は(ハック的に)シミュレーションジョブを活用することで、rvizを用いたSLAMの可視化やrvizを用いたNavigationを実現しています。 最終的には以下の構成で検証を行いました。 最終的な構成 技術的に検討をしたポイント 今回の取り組みを実現するために、技術的に以下の2点について検証をしました。 ①インターネット経由でSLAM/Navigationを動かすために必要なROSトピックを調べる ②AWS RoboMaker上でSLAM/Navigationを動かす。 それぞれについて説明します。 ①インターネット経由でSLAM/Navigationを動かすために必要なROSトピックを調べる まず、SLAMやNavigationを動かすためにどのようなROSトピックが使われているかを確認します。 SLAM に必要なROSトピックの確認 SLAMは turtlebot3_manipulation_slam を使用しました。デフォルトでは、 turtelbot3_slam_gmapping が起動されます。 以下のコマンドで起動します。 TURTLEBOT3_MODEL =waffle_pi roslaunch turtlebot3_manipulation_slam slam.launch 起動に成功すると下のような画面が表示されます。 SLAM turtlebot3_slam_gmapping がpublish/subscribeしているROSトピックの一覧 rosnode info で、 turtlebot3_slam_gmapping が使用している=subscribeしているROSトピックを調べます。 $ rosnode info /turtlebot3_slam_gmapping -------------------------------------------------------------------------------- Node [ /turtlebot3_slam_gmapping ] Publications: * /map [ nav_msgs/OccupancyGrid ] * /map_metadata [ nav_msgs/MapMetaData ] * /rosout [ rosgraph_msgs/Log ] * /tf [ tf2_msgs/TFMessage ] * /turtlebot3_slam_gmapping/entropy [ std_msgs/Float64 ] Subscriptions: * /scan [ sensor_msgs/LaserScan ] * /tf [ tf2_msgs/TFMessage ] * /tf_static [ tf2_msgs/TFMessage ] ( 以下略 ) ここから、 turtlebot3_slam_gmapping を動かすためには、実機側で発行されている /scan 、 /tf 、 /tf_static が必要ということがわかります。 ただし、試してみた限りでは /tf_static は必要ありませんでした。 Navigationに必要なROSトピックの確認 Navigationには、 turtlebot3_manipulation_navigation を使用しました。 以下のコマンドで起動します。 $ TURTLEBOT3_MODEL =waffle_pi roslaunch turtlebot3_manipulation_navigation navigation.launch map_file: = /tmp/my_map.yaml open_rviz: =false 起動に成功すると下のような画面が表示されます。 Navigation Navigationに使用されているROSトピックの一覧 ここでは、 amcl と move_base という2つのノードについて調べました。 $ rosnode info amcl -------------------------------------------------------------------------------- Node [ /amcl ] Publications: * /amcl_pose [ geometry_msgs/PoseWithCovarianceStamped ] * /diagnostics [ diagnostic_msgs/DiagnosticArray ] * /particlecloud [ geometry_msgs/PoseArray ] * /rosout [ rosgraph_msgs/Log ] * /tf [ tf2_msgs/TFMessage ] Subscriptions: * /initialpose [ unknown type ] * /scan [ sensor_msgs/LaserScan ] * /tf [ tf2_msgs/TFMessage ] * /tf_static [ tf2_msgs/TFMessage ] ( 以下略 ) $ rosnode info move_base -------------------------------------------------------------------------------- Node [ /move_base ] Publications: * /cmd_vel [ geometry_msgs/Twist ] * /move_base/DWAPlannerROS/cost_cloud [ sensor_msgs/PointCloud2 ] * /move_base/DWAPlannerROS/global_plan [ nav_msgs/Path ] * /move_base/DWAPlannerROS/local_plan [ nav_msgs/Path ] * /move_base/DWAPlannerROS/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/DWAPlannerROS/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/DWAPlannerROS/trajectory_cloud [ sensor_msgs/PointCloud2 ] * /move_base/NavfnROS/plan [ nav_msgs/Path ] * /move_base/current_goal [ geometry_msgs/PoseStamped ] * /move_base/feedback [ move_base_msgs/MoveBaseActionFeedback ] * /move_base/global_costmap/costmap [ nav_msgs/OccupancyGrid ] * /move_base/global_costmap/costmap_updates [ map_msgs/OccupancyGridUpdate ] * /move_base/global_costmap/footprint [ geometry_msgs/PolygonStamped ] * /move_base/global_costmap/inflation_layer/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/global_costmap/inflation_layer/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/global_costmap/obstacle_layer/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/global_costmap/obstacle_layer/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/global_costmap/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/global_costmap/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/global_costmap/static_layer/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/global_costmap/static_layer/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/goal [ move_base_msgs/MoveBaseActionGoal ] * /move_base/local_costmap/costmap [ nav_msgs/OccupancyGrid ] * /move_base/local_costmap/costmap_updates [ map_msgs/OccupancyGridUpdate ] * /move_base/local_costmap/footprint [ geometry_msgs/PolygonStamped ] * /move_base/local_costmap/inflation_layer/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/local_costmap/inflation_layer/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/local_costmap/obstacle_layer/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/local_costmap/obstacle_layer/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/local_costmap/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/local_costmap/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/parameter_descriptions [ dynamic_reconfigure/ConfigDescription ] * /move_base/parameter_updates [ dynamic_reconfigure/Config ] * /move_base/result [ move_base_msgs/MoveBaseActionResult ] * /move_base/ status [ actionlib_msgs/GoalStatusArray ] * /rosout [ rosgraph_msgs/Log ] Subscriptions: * /clock [ rosgraph_msgs/Clock ] * /map [ nav_msgs/OccupancyGrid ] * /move_base/cancel [ unknown type ] * /move_base/global_costmap/footprint [ geometry_msgs/PolygonStamped ] * /move_base/goal [ move_base_msgs/MoveBaseActionGoal ] * /move_base/local_costmap/footprint [ geometry_msgs/PolygonStamped ] * /move_base_simple/goal [ geometry_msgs/PoseStamped ] * /odom [ nav_msgs/Odometry ] * /scan [ sensor_msgs/LaserScan ] * /tf [ tf2_msgs/TFMessage ] * /tf_static [ tf2_msgs/TFMessage ] ( 以下略 ) これらの情報を精査した結果、SLAMで使用していたROSトピックに加えて、 /odom が必要ということがわかりました。 また、インターネット経由でTurtlebot3の実機を制御するためには、 move_base ノードがpublishした /cmd_vel を実機に届ける必要があることがわかります。 これらの情報から、AWS RoboMakerから弊社intdashサーバー経由でSLAM/Navigationを行うには、Turtlebot3実機とシミュレーションジョブで以下のように設定すればよいことがわかりました。 Turtlebot3は、 /scan 、 /tf 、 /odom をintdashサーバーにアップロードし、 /cmd_vel をダウンロードする。 AWS RoboMaker側のシミュレーションジョブは、 /scan 、 /tf 、 /odom をintdashサーバーからダウンロードし、 /cmd_vel をアップロードする。 帯域の計測 インターネットを経由する場合、どのくらいの帯域が必要になるか気になると思います。 弊社の製品には流れているデータの流量を計測できるものがあるので、それを用いてROSトピックの帯域を計測しました。 結果は以下のようになります。 /scan : 約40Kbps /scan /tf : 約74Kbps /tf /odom : 約23Kbps /odom /cmd_vel : 約1kbps /cmd_vel ここから、Turtlebot3の実機からROSトピックを流すために必要な帯域は、約140kbps、実機を制御するのに必要なROSトピックを流すために必要な帯域は約1kbps であることがわかりました。 上り・下りの帯域としては、現実的に利用可能な値ではないかと思います。 ただし、これらの帯域の値はTurtlebot3を使用した時の値であり、必要となる帯域は実際に使用するロボットの設定・構成に依存するので注意が必要です。 ②AWS RoboMaker上でSLAM/Navigationを動かす。 AWS RoboMakerでは、Gazeboを使ったシミュレーションだけではなく、ROS bag 再生によるシミュレーションを行うことが可能です。 今回の取り組みでは実機を使うので、Gazeboは不要です。ROS bag再生用のシミュレーションアプリケーションを作成し、利用します。 具体的なやり方について説明します。 ROS bag再生に対応したシミュレーションアプリケーションを作る AWS RoboMakerのコンソールから、「シミュレーションアプリケーション」→「シミュレーションアプリケーションの作成」を選択します。 シミュレーションソフトウェアスイートは「RosbagPlay」を選択します。このように設定することで、ROS bag再生用のシミュレーションアプリケーションを作ることが出来ます。 後ほどシミュレーションジョブを作成するときに使うので、アプリケーションに名前を付けます。ここでは「aptpod」という名前を付けました。 ROS bag再生用のシミュレーションジョブの作成 AWS RoboMakerのコンソールから、「シミュレーションジョブ」→「シミュレーションジョブの作成」を選択します。 今回は、ステップ2の「ロボットアプリケーションの指定」では「なし」を選択します。 ステップ3の「シミュレーションアプリケーションの指定」では、まず先ほど作成した既存のアプリケーションを選びます。 次に「データソースの設定」を行います。 コンソールから設定するときは、シミュレーションジョブを起動するために少なくとも一つのROS bagファイルを設定する必要があります。 ROS bagファイルはS3上にあるものを指定します。 AWS RoboMakerではlaunchファイルにROS bagを再生する設定を書くか、ターミナル上でROS bagファイルを再生しない限りは再生されないため、ここで設定したROS bagファイルは実際には再生されません。 これらの設定を終えたらシミュレーションジョブの作成を完了し、シミュレーションジョブが起動するのを待ちます。 シミュレーションジョブが起動すると、以下のような画面になります。 ここで、Terminalを選択するとターミナルが開かれます。あとは冒頭の動画で紹介したようにSLAMやNavigationなどのROSノードを起動するとrvizが使えるようになります。 まとめ 今回は「Turtlebot3の実機を使ったSLAMとNavigationをAWS RoboMaker上でやってみた」 について紹介しました。 研究開発グループではROSやAWS RoboMakerに関連した取り組みに限らず、プロトコルや機械学習に関連したテーマなど、様々な技術テーマの調査・検証を進めています。 今後も継続的に調査・検証の結果を記事として投稿できれば良いと思います。 最後までご覧いただきありがとうございました。
アバター
はじめまして!WEBチームの黒川と申します!昨年7月にaptpodに入りましてもうすぐaptpod歴1年になります! aptpodでは主にフロントエンドエンジニアとしてReact/TypeScriptを用いて、お客様向けアプリケーションのUI部分を実装しております。 ご存じの方も多いように、Reactの状態管理にはいくつか方法があり、何を用いるべきかなどでしばしば議論が起こりがちです。代表的なものだけでも、標準APIを用いる useState と Context やデファクトスタンダードとなってきている Redux 、そして新興の Recoil があります。 弊社のWEBチームではReduxを採用するケースが多いです。私もReduxについては一通りの知識と経験は持っていたつもりだったのですが、先日担当させていただいたプロジェクトで初めてReduxの設計に取り組んだところ、自分がReduxの思想や勘所について何も理解していなかったという事に気づきました。 そこで、同じくReduxの設計で悩んでいる方に向けて少しばかりのヒントになればと思い、私がReduxについて再勉強をする中で見つけた3つのTipsについて本記事にまとめました!(ただし、私自身が大規模開発の経験がないため、本記事は小〜中規模開発向けの内容になります🙇‍♂️) ※本稿のTipsは、公式の Redux Style Guide にも記載があります。Reduxの公式ページは本当にドキュメントが豊富なのでめっちゃ勉強になります。本稿では、抽象度の高い&英語の公式の記載について、なるべく分かりやすくお伝えすることを目的としています。 Redux Toolkitを使おう Storeに格納するデータは正規化しよう useSelectorはパフォーマンスを意識しよう 1. useSelectorでオブジェクトを返す時はshallowEqualを併用する 2. useSelectorを用いる時は大きいオブジェクトを一度に返すのではなく、細かく必要な値ごとに用いる 3. 重い処理にはreselectを用いる まとめ Redux Toolkitを使おう Redux Toolkit はRedux開発チームによる公式のツールキットです。 Redux 開発で困りがちな イミュータブルな状態の更新 TypeScriptの型定義 大量のボイラープレート 非同期処理 あたりが解消され、簡単かつコンパクトに書けるようになります。 特にボイラープレートの削減が個人的に最も嬉しい点です。Reduxを書く時には、 ducks パターン、あるいは Re-ducks パターンを用いて書く事が多いのではないかと思います。弊社でもRe-ducksパターンで書くことが多いのですが、Re-ducksパターンは1ディレクトリにボイラープレートとなるファイルが多いため煩雑さと管理の面倒さが生まれます。ducksパターンでは1ファイルにactionやreducerをまとめるため、煩雑さはありません。しかし、開発が進むにつれて多くのactionの数やreducerの処理が増え、1ファイルの持つ情報量が多くなりすぎます。 そこでRedux Toolkitです。以下に increment 、 decrement 、 addValue 、 subtractValue と4つのactionを持つカウンターのducksパターンについて、普通のReduxとRedux Toolkitを用いた2パターンで記述しました。 // 普通のRedux const INCREMENT = 'INCREMENT' const ADD_VALUE = 'ADD_VALUE' const DECREMENT = 'DECREMENT' const SUBTRACT_VALUE = 'SUBTRACT_VALUE' function increment () { return { type : INCREMENT } } function addValue ( value ) { return { type : ADD_VALUE , payload: value } } function decrement () { return { type : DECREMENT } } function subtractValue ( value ) { return { type : SUBTRACT_VALUE , payload: value } } function counter ( state = 0 , action ) { switch ( action. type) { case INCREMENT: return state + 1 case ADD_VALUE: return state + action.payload case DECREMENT: return state - 1 case SUBTRACT_VALUE: return state - action.payload default : return state } } // Redux Toolkit const counterSlice = createSlice ( { name: 'counter' , initialState: 0 , reducers: { increment: ( state ) => state + 1 , addValue: ( state , action ) => state + action.payload , decrement: ( state ) => state - 1 , subtractValue: ( state , action ) => state - action.payload , } , } ) いかがでしょうか。同じ内容でもRedux Toolkitが提供している createSlice を用いることで記述量がぐっと減りました。このSliceを1ファイルに1つ置いてducksパターンのように用いれば、煩雑さの解消と複雑さの解消が両立できるのではないかと考えています😊もしこのSlice+ducksパターンを用いても1ファイルの持つ情報量が大きく肥大化したのであれば、その時はSliceの責務が大きくなりすぎている可能性がありますので、Sliceの更なる分割を検討してみてもいいかもしれませんね! 以上のようにRedux Toolkitを用いるとReduxのファイル群管理が楽になります。また、前述のようにTypeScriptでちゃんと型定義されているので開発体験が良いことや、あらかじめ redux-thunk が組み込まれている点、また redux-devtools の設定が既にstoreになされているといった手軽さもあります。(勿論、これらが不要であれば剥がすこともできます!😄) 一方で「 immer.js に依存がある」や「そんなに記述量が減らないじゃん」、「ActionのtypeとActionCreatorが1対1で柔軟性に欠ける」などといった批判があるのも事実です。今の所、私個人としてはこれらのデメリットよりもメリットが上回りオススメしているのですが、開発に用いる際には、チーム内でメリット、デメリットを吟味した上で導入すると良いと思います! Storeに格納するデータは正規化しよう 状態管理に何を使うにしろAPIからデータを取得することはフロントエンドだと日常的にあることかと思います。 例えば、IoTのサービスで計測データ一覧を取得するAPIであれば、以下のような形のデータがAPIから返ってきます。 const measurements = [ { id: 'measurement1' , user: { id: 'user1' , name: 'HogeHoge' } , data: [ { id: 'meas1' , name: 'speed' , value: 100 , unit: 'km/h' , time: 1234567890 } , { id: 'meas2' , name: 'distance' , value: 1000 , unit: 'km' , time: 1234567891 } , // 似たような個々の計測単位データがずっと続く ] } , { id: 'measurement2' , user: { id: 'user2' , name: 'FugaFuga' } , data: [ { id: 'meas3' , name: 'time' , value: 10 , unit: 'seconds' , time: 1234567892 } , { id: 'meas4' , name: 'fuel' , value: 30 , unit: 'litre' , time: 1234567893 } , // 似たような個々の計測単位データがずっと続く ] } // 似たような計測データがずっと続く ] これはAPIから取得する時はさほど問題になりませんが、Storeに格納する時には以下の理由で問題になります。 各データが散逸して保存されているため、どこかを更新した時に関連する値が全て更新されているか確証を得にくい データが多重ネスト化されていることから、構造が複雑であることに加えてデータの更新に時間がかかる 特に弊社で扱うような計測ではデータは平気で数千、数万以上になりますので、あるidを持つ計測を配列から探してその値の一部を更新して…などといった操作はパフォーマンスを大きく悪化させます イミュータブルなデータの更新はそのデータの全ての親要素の更新も伴います。 return {...state, state.hogehoge} で state.hogehoge だけではなくマージされた state も更新されるためです。なので、ネストが深くなれば深くなるほど不必要なデータの更新が起こることになります。 じゃあどうするのかというと、ReduxのStoreをリレーショナルデータベース(以下、DB)のように扱い、格納する値を正規化すれば良いのです。 フロントエンドは普段DBに対してサーバーサイドを通して接しているため、正規化を意識することが少ないです。なので、ここで正規化についておさらいすると、 正規形と呼ばれるルールによりデータの一貫性の維持を実現し、データアクセスの効率化、冗長性と不整合の排除を目的とする 第一正規形〜第五正規形まであるが、多くの場合第三正規形までで実用に足る 第一正規形:各カラムにデータが一つだけ入っており、DBに格納できる状態 第二正規形:各テーブルが部分関数従属な要素を分割して完全関数従属な状態にする 関数従属:ある要素が決まる時に他の要素が決まる時、関数従属となる(例: A -> B) 部分関数従属:複数の候補キーがある中で一部が関数従属の時、部分関数従属となる(例: (A, B) -> C の時に A -> C または B -> Cの場合) 第三正規形: 各テーブルから推移的関数従属を排する 推移的関数従属: 非キー属性同士で関数従属性がある状態(例: A -> B -> C) 以上を踏まえて冒頭のデータを正規化した値が以下になります。 const measurements = { measurement1: { id: 'measurement1' , userId: 'user1' , data: [ 'meas1' , 'meas2' ] } , measurement2: { id: 'measurement2' , userId: 'user2' , data: [ 'meas3' , 'meas4' ] } , } const users = { user1: { id: 'user1' , name: 'HogeHoge' } , user2: { id: 'user2' , name: 'FugaFuga' } } const measurementData = { meas1: { id: 'meas1' , name: 'speed' , value: 100 , unit: 'km/h' , time: 1234567890 } , meas2: { id: 'meas2' , name: 'distance' , value: 1000 , unit: 'km' , time: 1234567891 } , meas3: { id: 'meas3' , name: 'time' , value: 10 , unit: 'seconds' , time: 1234567892 } , meas4: { id: 'meas4' , name: 'fuel' , value: 30 , unit: 'litre' , time: 1234567893 } } ※dataだけは多対多の関係は中間テーブルよりも配列の方がスマートなためidの配列で表しています Storeの正規化を行うことで嬉しいポイントとして、以下が挙げられます。 Storeの整合性が常に取れている ネストが浅いので複雑性がない。また、値の更新に伴う不必要な値の巻き込み更新が最小限になる それぞれの要素が持つ個別の値について(例: measurementData の meas1 など)、 filter などを用いなくてもダイレクトに参照可能であり更新可能である 自分はこの正規化した各要素ごと( users や measurementData )に前述のSliceを切ると良い感じにStoreを持てるのではないかと考えています。また、正規化した値を作るのが大変な時は、 normalizr のようなライブラリを使うこともReduxは勧めています。実際に、TwitterはStoreへの格納にnormalizrを使用しているそうです。 useSelectorはパフォーマンスを意識しよう ReduxもHooks時代なので useSelector と useDispatch を用いれば、 connect でラップしなくてもReactとReduxの接続が可能となりました。ただし、便利な反面、 useSelector は何も考えずに用いると connect よりもパフォーマンスが低下します。意識したいベストプラクティスとしては、 useSelector でオブジェクトを返す時は shallowEqual を併用する useSelector を用いる時は大きいオブジェクトを一度に返すのではなく、細かく必要な値ごとに用いる 重い処理にはreselectを用いる 以下、個別に説明します。 1. useSelector でオブジェクトを返す時は shallowEqual を併用する useSelector は内部にキャッシュを持っていまして、selectorが返す値が前回と等しければキャッシュされた前回の値を返します。ただし、前回の値と新しい値の比較は、 === 演算子で行われます。ここがミソです。 JavaScript/TypeScriptではプリミティブな値(stringやnumber)であれば、値が等しければ === で比較した時に true となりますが、オブジェクト(ObjectやArray)は 値が等しくても === で比較した時に異なるインスタンスであれば false となります。以下が例になります。 const a = { val: 'hogehoge' } const b = { ...a } // {val: "hogehoge"} console.log ( a ) // {val: "hogehoge"} console.log ( b ) // true console.log ( a === a ) // false console.log ( a === b ) Reduxでは常に値の更新が発生した時にオブジェクトは別のインスタンスとなっています。したがって、selectorから返されるオブジェクトの比較は常にfalseとなり再レンダリングが走ります。また、Containerに接続しているComponentにも異なるオブジェクトが渡されることにより再レンダリングが生じ、これによりパフォーマンスに悪影響が生まれます。 ※ちなみに react-redux で従来使われていた connect のmapStateではオブジェクト内の値で比較が行われていたため、この問題は発生しませんでした ※Containerに接続されているComponentは React.memo でラップされているものと考えます そこで用いるのが shallowEqual になります。これは、react-reduxで標準に提供されている比較関数でして、これを用いると異なるインスタンスのオブジェクトであっても同じ値であれば true を返してくれます。( React.memo の比較関数に用いられているものも中身は違うかと思いますがShallow Equalですね!😄) ※比較関数は shallowEqual 以外にも任意のものを使用できます ※Shallow Equalは多重ネストまでは比較できないので、先程の正規化がここでも重要になります import { shallowEqual } from 'react-redux' const a = { val: 'hogehoge' } const b = { ...a } // {val: "hogehoge"} console.log ( a ) // {val: "hogehoge"} console.log ( b ) // true console.log ( shallowEqual ( a , a )) // true console.log ( shallowEqual ( a , b )) これにより不要な再レンダリングを抑止できて、パフォーマンスの改善に繋がります👌 2. useSelectorを用いる時は大きいオブジェクトを一度に返すのではなく、細かく必要な値ごとに用いる 上述の通り、 useSelector では渡されたselectorの返り値が異なる場合に再レンダリングが走ります。なので、もし大きなオブジェクトを useSelector で返し、その一部をContainerでは使用するという書き方をすると、オブジェクト内の一部の値が別のContainerで変更された場合であっても、値の変更がそれを含むオブジェクトの変更を起こし、接続されたComponentの再レンダリングが生じます。 以下の例で説明しますと、ContainerAでは val1 と val2 しか用いていませんが、ContainerBにおいて val3 を更新するとStateそのものに更新がかかるためContainerAにも更新がかかります。 type State = { val1: string val2: string val3: string } const ContainerA = () => { const allState = useSelector (( state: State ) => state ) const val1 = useMemo (() => allState.val1 ) const val2 = useMemo (() => allState.val2 ) } const ContainerB = () => { const val3 = useSelector (( state: State ) => state.val3 ) } 上記のケースの場合、ContainerAで必要な値の分だけ useSelector で切り出すと、 val1 と val2 はプリミティブな値であるstringなので useSelector の値は変わらず再レンダリングはかかりません。場合によっては、 useSelector からオブジェクトを返す必要もあるかと思いますが、その場合は前述の shallowEqual を用いて可能な限り再レンダリングは避けるようにしましょう。 3. 重い処理にはreselectを用いる ここまでの説明で useSelector を適切に用いることでComponentの再レンダリングを防げることがわかりました。しかし、もう一点考慮すべきポイントがあります。selectorそのものの計算量です。 useSelector はselectorのインスタンスが変わらない限り再計算を行いません。しかし、以下のように useSelector 内のselector関数がインラインの場合は、毎回レンダリングがかかるたびにselector関数が新しいインスタンスとなるため再計算が行われることとなります。 const hoge = useSelector ( state => state.hogehoge ) ややこしいのですが、 useSelector はactionがdispatchされるたびにselectorの返り値について参照比較が行われ、もし返り値が異なれば再レンダリングをかけます。すなわち以下の例のように、自身は値の変更が生じていないのに、再レンダリングに引っ張られて再計算するパターンがあるのです。 //どこかでhogeの新しい値がdispatchされる dispatch ( updateHoge ( hoge )) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // hogeに変更がかかるactionがdispatchされ、当然hogeを変更するため再レンダリングが行われる const hoge = useSelector (( state ) => state.hoge ) // fugaは変更がないためuseSelectorの返り値は一緒だが、selector関数のインスタンスが新しくなり、再計算が走る const heavyFuga = useSelector (( state ) => heavyCalculation ( state.fuga )) 軽量なただstateの一部を返すselectorであれば何も問題ありませんが、上記のように重い処理を行っているselectorであればパフォーマンスへの影響が生まれます。このような問題に対処すべく、selectorで何かしらの加工を行うのであれば reselect を使用し、selectorの計算もまたメモ化することが好ましいです。 reselectを用いると、メモ化されるため値の更新が無い限りオブジェクトでもインスタンスは変更されず、先程の shallowEqual がなくても再レンダリングが抑制されます。極端な話、reselectを全てに用いて、大きなオブジェクトで切り出さなければあまりパフォーマンスについて意識する必要はありません。しかし、reselectでメモ化する際には少なからずメモ化のための処理が負荷されます。 あまり大きな負荷ではないので、神経質になる必要はないとは思いますが、個人的にはただ値を返す useSelector は useSelector(state => state.hoge) といった普通の書き方で行い、切り出した値に何かしらの処理を加える場合は、reselectを用いるのが適切かなと思います。 まとめ Reduxは詰まるところ大きな reduce 関数であるということが改めて勉強することでやっと腹落ちできました。初期値として与えられたinitial stateにアクションで定義された処理を通して、また新たなStateを獲得する。結局はこれの繰り返しであり、 reduce 関数が自由度の高い関数であると同様に、Reduxも非常に自由度が高く、全然アンチパターンであっても実装はできます。ただし、アンチパターンだとパフォーマンスに劣り、メンテナンス性も低く、良いアプリケーションにならないため、常に良い設計を意識する必要があると認識できた次第です。 ここでは私がReduxを改めて勉強して気付いたこと、導き出したことをTipsとしてまとめました。本稿は現時点で自分の考える最良のアプローチですが、Reduxに強い人から見るとまだまだブラッシュアップすべき点が数多くあると思います。もし改善すべき点や誤っている点などありましたら、本記事からのツイートやはてブでお知らせいただけると幸いです。 個人として今後ももっともっとフロントエンドについて学び、本テックブログを通して意見を交わしていければと思っておりますので、よろしくお願い致します!
アバター
Webチームの蔵下です。Chrome 81で Web NFC が試験的に導入されました! ちょっと変わり種なのでネット上ではあまり話題にならなかったのですが、個人的にはビッグニュースでした。 Web NFCを使うと、下記のTweetのような実在するカードとWebサイトを組み合わせたゲームなどが実装できます! すごい! 🏷️ Web NFC reaches a key milestone - it is coming soon! Check out https://t.co/wC4Sx6Rpu8 pic.twitter.com/MmsIDHGNjy — Chrome for Developers (@ChromiumDev) 2019年12月17日 勢いのままにWeb NFCを触ってみたので、ソースコードを交えて使い方を紹介します。 Web NFCとは? Web NFCとは、JavaScriptでWebサイトからNFCタグにデータを読み書きできるAPIです。記事公開時点(2020年6月)ではAndroid Chromeでの対応となっているため、NFC機能が搭載されているAndroidスマートフォンで動作が確認できます。 NFCタグ NFC(Near Field Communication)とは近接型無線通信方式で、デバイスでNFCタグにタッチするだけでデータの読み書きができます。今回Web NFC確認用に Amazon で購入したNFCタグは、容量が144Byteと小さいですが価格も安く手軽に試せます。 購入したNFCタグ。シールになっていて貼り付けられる。 Web NFCを試す前の下準備 Web NFCはまだ試験的な導入のため、 chrome://flags/#enable-experimental-web-platform-features のフラグを Enabled にするか、 Origin Trials の設置が必要です。 chrome://flags/#enable-experimental-web-platform-featuresをEnabledに変更 Web NFCのソースコード Web NFCを実際に試したソースコードを紹介します。 Scan NFCタグからデータを読み込むには、 NDEFReader の scan() でScan機能を起動します。起動した状態でデバイスでNFCタグにタッチすると reading Eventが発火し、NFCタグに付与されているシリアルナンバー( serialNumber )と書き込まれているデータ( message )が受け取れます。一度Scanすると、NFCタグにタッチする度にEventが発火します。 const scan = async () => { try { const reader = new NDEFReader() await reader.scan() // Scanは起動しているが、NFCタグからデータが読み込めなかった reader.addEventListener( 'error' , ( event ) => { console.log(error) } ) // データを読み込んだ reader.addEventListener( 'reading' , ( { serialNumber, message } ) => { const record = message.records [ 0 ] const { data, recordType } = record // recordTypeごとにdecode処理を実行する } ) } catch (error) { // Scan起動失敗 console.error(error) } } 受け取った message の中に、NFCに書き込まれていたデータが records として配列で格納されています。複数のデータが書き込まれている場合、 records に複数個の record が入ります。 record に付与されている recordType でデータの種類が特定できるので、 recordType ごとにdecode処理を実装します。 ※ 初めて Scan を実行する際に下記のようなダイアログが表示されます NFC機能確認ダイアログ Text recordType が text の場合はTextデータとなります。 TextDecoder でdecodeすることで、 data を文字列として扱えます。 const { data, encoding, recordType } = record if (recordType === 'text' ) { const textDecoder = new TextDecoder(encoding) const text = textDecoder.decode(data) console.log( `Text: ${text} ` ) } JSON recordType が mime 、 mediaType が application/json の場合はJSONデータとなります。 JSON.parse() でparseすることで、Objectとして扱えます。 const { data, mediaType, recordType } = record if (recordType === 'mime' && mediaType === 'application/json' ) { const textDecoder = new TextDecoder() const json = JSON.parse(textDecoder.decode(data)) console.log(json) } Image recordType が mime 、 mediaType が application/png の場合はPNGデータ(Image)となります。 data を Blob へ変換し、 image.src に URL.createObjectURL() で渡すことで画像が表示できます。 const { data, mediaType, recordType } = record if (recordType === 'mime' && mediaType === 'image/png' ) { const blob = new Blob( [ data ] , { type: mediaType } ) const image = new Image() image.src = URL.createObjectURL(blob) } Write NFCタグへデータを書き込むには、 NDEFWriter の write() を実行し、デバイスをNFCタグにタッチします。データは文字列で書き込まれるため、データの種類ごとにencode処理が必要になります。 Text Textは文字列のため、そのまま write() で書き込みます。 const writeText = async(text) => { try { const writer = new NDEFWriter() await writer.write(text) } catch (error) { console.error(error) } } JSON dataは JSON.stringify() で文字列へ変換し、 recordType , mediaType を付与した record を作成します。作成した record を records 配列に格納して write() で書き込みます。 const writeJson = async(data) => { const encoder = new TextEncoder() const jsonRecord = { recordType: 'mime' , mediaType: 'application/json' , data: encoder.encode(JSON.stringify(data)), } try { const writer = new NDEFWriter() await writer.write( { records: [ jsonRecord ] } ) } catch (error) { console.error(error) } } Image 書き込みたいImageを ArrayBuffer へ変換し、 recordType , mediaType を付与した record を作成します。作成した record を records 配列に格納して write() で書き込みます。 const writeImage = async (url) => { const imageRecord = { recordType: 'mime' , mediaType: 'image/png' , data: await (await fetch(url)).arrayBuffer(), } try { const writer = new NDEFWriter() await writer.write( { records: [ imageRecord ] } ) } catch (error) { console.error(error) } } Imageを書き込む方法は上記のように用意されていますが、NFCタグは一般的に数百バイトと容量が小さく、私達が普段扱っているような画像は書き込めません。今後のNFCタグの性能向上に期待したいですが、しばらくは画像をNFCタグへ直接書き込むことは難しいでしょう。 まとめ デバイスをタッチしてアクションを起こすという動作は、スマートフォンや交通系ICカードなどの普及もあり、私達の生活で当たり前のものとなりました。今まではネイティブのアプリケーションでしか実装できなかったNFCのデータ通信も、JavaScriptを使ってWebサイトで実装できるようになると、新しいサービスや体験が生まれてくるかもしれません! 今後も変わり種JavaScriptの情報を発信していきますのでご期待ください! 採用情報 アプトポッドでは、一緒に働いてくださる仲間を募集中です。 弊社は、IoT(Internet of Things)という言葉がバズるはるか以前、当時まだM2M(Machine-to-Machine)と呼ばれていたような時代から、IoTミドルウェアやIoTデータの可視化ダッシュボード開発を手掛けてきた、IoTプロダクトのスペシャリスト集団です。(弊社の成り立ちについては、弊社VPoPが投稿しているこちらの記事 アプトポッドの過去と現在、そして未来について - aptpod Tech Blog をご覧ください) 弊社の主力製品は、 IoTプラットフォームの構築用ミドルウェア intdash と、そのミドルウェアを流れる大量のIoTデータを可視化する Webベースのダッシュボード Visual M2M Data Visualizer です。リンク先の弊社サイトでもご紹介している通り、自動車やロボットから収集した秒間数千点にもなるIoTデータを、リアルタイムに遅延なくブラウザ上に描画するパフォーマンスに強みを持っています。 www.aptpod.co.jp サーバーから WebSocket や WebTransport をベースとした独自プロトコル iSCP(intdash Stream Control Protocol)によってプッシュ型で配信されてくる大量のデータには、数千Hzにもなるセンサーデータだけでなく、映像データや音声データなど、様々なものを含みます。これらをブラウザ上で統合的に可視化することによって、IoTデータの利活用を強力にサポートします。 弊社のWebアプリケーション開発は、大量に送りつけられるデータの捌き方、様々な種類のマルチモーダルデータの取り扱い、独自プロトコルのハンドリングなど、なかなか他では体験できない開発トピックが盛りだくさんです。 情報技術の総合格闘技とも言われるIoTの分野で、我々と一緒に、技術で未来を切り開いてみませんか? 応募は、当社リクルートページよりお願いします。 www.aptpod.co.jp
アバター
先進技術調査グループのエンジニアの酒井 ( @neko_suki )です。 過去に2回、Turtlebot3の遠隔制御について紹介をしました。 tech.aptpod.co.jp tech.aptpod.co.jp 今回は、ネットワークの切断やほかの要因によって大幅な通信遅延が発生した際に、Turtlebot3を安全に遠隔制御技術する技術として以下の2点についてご紹介します。 ①フェールセーフ機能 ②メッセージの遅延への対応 Turtlebot3の遠隔制御は、PS3コントローラからのメッセージをクラウド経由でTurtlebot3に届けることで実現しています。 PS3コントローラはRaspberry Pi3にBluetoothで接続されています。そのRaspberry Pi3上で弊社製品のintdash Edgeが動作しています。intdash EdgeはTurtlebot3側のRaspberry Piにも搭載されています。この二つが弊社のクラウドを経由して通信を行っています。 全体の構成 Wi-FiやLTEなどの公衆無線では、通信環境が悪くなると、ネットワークが一時的に切断されたり、遅延が発生することがあります。 そのような状況で発生する課題とその対応策として、以下の2点について検討しました。 ①フェールセーフ機能 ②メッセージの遅延への対応 ①フェールセーフ機能の検討 フェールセーフ機能とは何か Turtlebot3のようにロボットを遠隔制御する場合、Wi-FiやLTEなどの公衆無線では、通信環境が悪くなるとネットワークが一時的に切断されたり遅延が発生することがあります。そのため、そのような状況を想定して安全に制御するためのフェールセーフ機能が必要になります。 以下の動画で、実際に実機を使い意図的にネットワークを切断した状態を発生させて何が起こるのかを確認しました。動画では、タイヤが制御されるようにPS3コントローラのスティックを固定しながら、Turtlebot3に接続している有線を抜きます(注: Wi-FiはOFFにしています)。有線を抜いた後もタイヤが動き続ける様子が確認できます。 www.youtube.com 動画では台の上にTurtlebot3を固定していますが、もし実際に地面の上を動いているときにネットワークの切断などが発生すると、そのまま動き続け障害物や壁などに衝突する可能性があります。 フェールセーフ機能対応前の処理フロー Turtlebot3では、PS3コントローラ側のRaspberry Piから送信された joyメッセージ をintdash Edgeを経由して rosbridge_tcp で受け取ります。 rosbridge_tcp は受け取った joyメッセージ をpublishします。publishされた joyメッセージ は、タイヤを制御する teleop_twist_joy ノードと アームを制御する teleop_arm_node ノードにsubscribeされます。 ノード構成 teleop_arm_node は受け取った joyメッセージ から、移動後のアーム座標を計算した joint_trajectory_point という Float64MultiArray型 のメッセージを生成してpublishします。 アーム制御(ROSノード) Turtlebot3はその座標情報をもとに、モーターを動かします。 アーム制御の結果 一方、 teleop_twist_joy は受け取った joyメッセージ から、速度を計算した cmd_vel という Twist型 のメッセージを生成してpublishします。 タイヤ制御(ROSノード) 速度と角度を基に、タイヤの制御が行われます。 タイヤ制御の結果 アームの制御では、1つの joy メッセージから移動後の座標を計算するため、ネットワークの切断や遅延によってメッセージが到達しない場合には、アームは動作しません。 通信切断時のアーム制御(ROSノード) 通信切断時のアーム制御 しかし、タイヤの制御はそうはなりません。ネットワークの切断や遅延によってメッセージが到達しない場合には、過去に設定された速度・角度の情報がそのまま使われます。 通信切断時のタイヤ制御(ROSノード) その結果、例えば障害物などがあると避けられずにぶつかってしまいます。 通信切断時のタイヤ制御の結果 フェールセーフ機能対応後の処理フロー フェールセーフ機能に対応するために、 cmd_vel_stopper というROSノードを追加しました。 cmd_vel_stopper はネットワークの切断や遅延を検出するために、一定時間 Joyメッセージ が受信できなかった場合に、速度を0にした cmd_vel をpublishするようにします。速度が0の cmd_vel をpublishすると、タイヤの制御が停止します。 ROSノードの構成は以下のようになります。 cmd_vel_stopper も joy メッセージをsubscribeします。 cmd_vel_stopper は Joyメッセージ を受信したタイミングを記録します。 そして内部で、100msec毎に最後に Joyメッセージ を受信したタイミングを監視します。もし現在時刻と最後に Joyメッセージ を受信したタイミングが200msecよりも大きいなら、速度を0にした cmd_vel をpublishします。これによって、タイヤを停止します。 フェールセーフ機能を実装したROSノード構成 フェールセーフ機能を入れた結果 実際に実機を使って確認した動画が以下の動画になります。先ほどとは異なり、有線を抜くとタイヤが止まることが確認できました。 www.youtube.com これによって、ネットワークの切断や遅延があってもTurtlebot3を安全に停止させるフェールセーフ機能が実現できました。 ②メッセージの遅延への対応 メッセージ遅延による課題 次は、メッセージが遅延した場合の処理について考えます。 現在の実装では、コントローラ側から20ms間隔で Joyメッセージ を発行します。この Joyメッセージ はクラウドを経由するためネットワーク遅延分だけ遅れてTurtlebot3 に到達します。 ネットワーク遅延 Joyメッセージが PS3コントローラ側のRaspberry PiからTurtlebot3に到達するまでに、一時的なネットワークの切断などによって大きな遅延が発生したとします。そうすると、ネットワークの状況が回復した際に、過去の Joyメッセージ が含まれた複数のメッセージが到達します。 そのまま Joyメッセージ をpublishすると、遅延によるレスポンスの悪化と、メッセージ間隔が維持されないことによる再現性の悪化の2つの問題が発生します。 レスポンスが悪化すると、例えば10秒の切断が発生したら10秒後を予想して操作をしなければいけません。 また、操作の再現性が失われると、意図しない動作になってしまう可能性があります。 したがって、ここにも対応が必要になります。 課題 メッセージ遅延への対策案 対策案1 対策案1では受信側で間隔をとって joyメッセージ をpublishします。 複数のメッセージを同時に受信した場合にそのままpublishすると、メッセージの間隔が変わってしまうため操作側の意図した通りに制御することが出来ません。なので、この案では1つ前の Joyメッセージ をpublishした時刻から20ms経過していない場合は20msec経過してから次の Joyメッセージ をpublishします。 対策案1 この案のメリットは、遅延が発生しても正確な動作が行える点です。 一方で、一度遅延が発生するとその遅れは継続して引き継がれるというデメリットがあります。これにより、レスポンスが非常に悪くなります。 対策案2 対策案2では遅延が大きいjoyメッセージは使用しないで破棄します。今のところ受信側に届くまでにメッセージを捨てる仕組みは用意されていないため、受信側での判断が必要になります。 対策案2 この案のメリットは、遅延の解消後はリアルタイム性のある制御が可能になる点です。 一方でデメリットは、途中で捨てた分の Joyメッセージ が処理されなくなる点です。その結果、ネットワークが切断していた期間の動作は実行されなくなります。 Turtlebot3の遠隔制御はデモ用途で使用しています。したがって、所定の動作を確実に再現することよりもリアルタイム性の維持の方が重要になります。 そのため、今回は対策案2を採用しました。 具体的には、PS3コントローラ側のRaspberry Piで設定したタイムスタンプと、Turtlebot3側で受信した時点でのタイムスタンプを比較します。もしこの差が1秒以上離れていたら何等かの遅延が発生したと判断しその Joyメッセージ はpublishしません。 この機能を追加するために rosbridge_tcp の使用をやめて、intdash Edgeから Joyメッセージ を直接取得するようにしました。 対策案2 ノード構成 この処理を実現するためにはPS3コントローラ側のRaspberry PiとTurtlebot3のRaspberry Piの時刻は同期が必要です。同じntpサーバーを使用して時刻同期をしているため、1秒以上の遅延が発生しているような状況を異常と判断するのはデモ用途においては妥当だと思います。 まとめ 今回は通信遅延発生時にTurtlebot3を安全に遠隔制御する技術についてご紹介しました。 先進技術調査グループではロボットの遠隔制御に限らず、プロトコルや機械学習に関連したテーマなど、様々な技術テーマの調査・検証を進めています。 今後も継続的に調査・検証の結果を記事として投稿できれば良いと思います。 最後までご覧いただきありがとうございました。
アバター
はじめに デザイン室 @tetsu です。 弊社aptpod製品 Visual M2M (以下VM2M) について、ありがたいことにお客様から 「デザインが良い」 とお褒めをいただくことが多いとのことで、デザインについての記事を書いて欲しい。 そんな話で営業チームから おだてられて オファーがありまして、本記事を書かせていただきます。 VM2M初期のコンセプトから現在の形にいたるまで、振り返りながら書いてみようと思います。 「デザインが良い」 と言われた時に、ヴィジュアル(全体の雰囲気、カラー、レイアウトなど見た目の印象)のことを言われている方と、使い勝手を含めたシステム全体のことを指して言われている方といらっしゃると思います。 この記事では、主に前者の ヴィジュアル面のデザインに寄った形で、UI(ユーザーインターフェース)の設計 について書いています。 前提条件として、お客様のタッチポイントであるUIが美しくストレス無く機能するためには、バックエンドのシステムが安定して稼働していることが不可欠です。 aptpodとしての一番の強みもそのバックエンドシステム intdash にあります。 aptpodのシステムの場合、もうひとつのタッチポイントであるエッジ・デバイス側からのデータが上がってこないと、何もはじまりません。 これらの前提条件が優秀なエンジニアによって支えられていることにより 「デザインが良い」 が実現していることに、デザイナーとしてはいつも感謝しています。 はじめに VM2Mとは VM2M変遷 VM2M 1st VM2M 2nd Generation VM2M Data Visualizer デザインにあたって データ設定 自由で汎用的なレイアウト 計測データの読み込み/ライブ表示 タイムライン 作業スペースの保存 ドラッグ&ドロップ 全体のコンセプト デザインする上でのお題 ダークな背景 フォント 失敗 操作系 Visual Parts ワークフローを作りきれなかった 今後 intdashユーティリティ VM2M Data Visualizer おわりに デザイン室の求人 VM2Mとは データを可視化するためのダッシュボードであり、Webブラウザ(Google Chrome)で動作するWebアプリケーションです。 用途に応じてカスタマイズ可能なUIで、様々なデータを可視化、解析することができます。 時系列データをライブストリーミング形式で表示、もしくは過去の計測データを読み込んで表示します。 様々な可視化パーツを用いて自由なレイアウトを組むことができます。 VM2M変遷 VM2M 1st 1stとは銘打っていませんが、後に2ndができたので区別するためにこう呼んでいます。 この時点では、 Webアプリケーションとしてだけではなくバックエンドのクラウドシステムも含めたSaaS として、VM2Mと呼称しておりました。 僕としては、旧知であった代表の坂元と久々に会った際に「すごいの作ってるから見て」と言われたのが最初のかかわりです。 ブラウザのアプリケーションでこんなにリッチなデータ可視化ができるんだと、感動したことを覚えています。 2014年の夏前ぐらいの季節だったように記憶していますが、その流れで秋口くらいからデザイナーとしてジョインすることになりました。 現在ではほとんど動いていませんが、フレームワークとして導入された案件など、いくつかのインスタンスで稼働しています。 VM2M 2nd Generation UIを全面的に刷新しました。 より汎用的に、美しく、未来的に。 そのデザインコンセプトや設計については後述します。 可視化パーツなどは1st時にCanvasで描画していたものをSVGに変更しました。 ベクターデータで描画することにより、高画質化していくディスプレイ環境に対して、拡縮にも耐えうる美しい表現を可能にしました。 VM2M Data Visualizer バックエンドシステムとしてintdashがリリースされ、フレームワークとしてのVM2Mは役目を終えました。 可視化ツールとしてVM2M Data Visualizer と名称を変えることになりました。 2ndの発展形で、機能や特徴に大きな変更はありません。 VM2Mという名称は aptpodのアプリケーション製品のブランド総称 として残っています。 当Web可視化アプリケーションの他に、iOSのアプリの VM2M Motion などいくつかのアプリケーションがあります(この辺の名称については今後マーケティング的に整理されていく可能性もあります)。 以上のような経緯ゆえに、当Webアプリケーションのことを指してVM2Mと言われていることも多いかと思います(社内ではData Vizと言われています)。 デザインにあたって データ設定 データをダッシュボードで見るためには、 エッジ(対象のデバイス) データ(パース対象) 可視化パーツ を紐付ける必要があります。 それぞれ表示されるリストから選択します。 たとえば、ある自動車の車速を見る際に、対象となる車とCANからくる車速の信号データ、表示するパーツとしてメータを選択します。 データを可視化するパーツとしては、ライブで瞬時値などを見る際にはメーター表示が適しているケースがあります。 時系列で相関関係を見る場合には、たとえばRPMなど他の関連データと重ねてグラフ表示するかもしれません。 メーター表示とグラフ表示の切り替え例(データはスマートフォンの加速度センサー) これらをひとつのパネルで自由に変更できるためのUIを考えました。 パーツごとに設定項目が異なるため、それぞれ個別に項目を設定できる案を初めは考えましたが、この時点ではなるべく汎用的な作りにしたかったので、サイドに共通の設定メニューを設け、パーツごとに属性をもたせる作りとしました。 統一されたUIですが、グラフとメーターでは別々の設定項目が表示されます。 ※地図パーツなど一部特殊な設定が必要なパーツが存在します。 VM2Mマニュアル Visual Parts データに関しては事前にパースするための設定が必要です。 VM2M(及びintdash)の基本的な設計として、通信間はバイナリデータのまま伝送し、クライアント(ブラウザ)側でパースします。 そのために必要なデータ定義ファイルを事前に設定します。 このような設計になっているのは、たとえば自動車のデータの場合など、パースするために必要なDBCファイルの多くはメーカーごとに社外秘であり、アプリケーションの開発当初(2014年〜)から現在に至るまで、クラウドにアップロードすることに制約があることが多いためです。 サーバー側でパース後の計算処理が必要なケースにはAnalytics Servicesを用いて変換しています。 自由で汎用的なレイアウト ユーザーによって求めるダッシュボードが異なる中で、画面の構成をどのようにすれば汎用性を保ちながらも雑然としないか苦心しました。 割り切ってウィンドウサイズなどを全てユーザーに委ねる方法も検討しました。 全て可変になり自由度がある反面、必要な情報を表示しきれないがゆえのユーザビリティの低下と、雑然と散らかるパネル群は美しくないという懸念がありました。 1stでは決められたグリッドに固定のマス目でそれぞれパーツを設定できる様になっており、整然とデータが表示される様は、前述の通り美しく、ひとつ完成度の高い形でした。 しかし当初から、注目したいパネルを大きく表示したいという需要が、特に動画データに対応したことにより高まっており、1stでは別ウィンドウで2倍の大きさで表示するような対応を取っていましたが、この課題を根本的に解決する必要がありました。 可変サイズのパーツを表示できるようにしつつ、全体のレイアウトとして整然と整う形を目指す上で、グラフィックデザインにおける グリッドシステムをUIへ適用 しました。 グリッドシステムとはヨゼフ・ミューラー=ブロックマンによって提唱されたレイアウト技法です。 グリッドというのは、格子状のガイドラインです。 グリッドシステムによる秩序が、「美しさ」と「伝達機能」を生み出します。 レイアウト上の仮想のグリッドを見えるように表示し、制約の範囲での自由なレイアウトをユーザーに提供します。 これにより、必要な情報の表示とデザインをコントロールしつつ、カスタマイズ可能なダッシュボードを作ることができるようになりました。 計測データの読み込み/ライブ表示 対象の計測(ライブか過去か、過去の場合はいつの計測か)を選択するUIが必要になります。 ライブの場合は、現在ストリームされているデータをリアルタイムに表示します。 ライブ表示 上部のLIVEアイコンをクリックでライブモードに切り替え 計測データ一覧 下部のボタン Stored Data をクリック(ショートカットキー D )で過去の計測データを表示 ライブ表示の終了で過去データの一覧を表示 計測データ一覧 読み込みを開始するための再生ボタンや一時停止ボタン、再生速度などのUIに関しては、世の中にある音楽プレイヤーなどを参考にしつつ、極力シンプルな形を模索しました。 タイムライン シークバーで過去の計測データを時間軸から操作 タイムライン に基準データを設定可能 フォーカスした時間帯を拡大、切り取り可能 マウス操作で直感的に扱えるようにすることと、キーボードの ◀ ▶ 操作や数値入力で細く設定できることを両立させるためのUIを考える上で、動画の編集アプリケーションなどを参考にしました。 作業スペースの保存 設定したデータやレイアウトなどの作業スペースに関する情報を保存する必要があります。 これらを Screen として下部のボタン Screen (ショートカットキー S )を押すことで呼び出します。 数値データを並べてみたい場合と、地図などにフォーカスした状態で見たい場合と、それぞれスクリーンを切り替えて表示することができます。 スクリーンはデフォルトでいくつかテンプレートを用意しました。 スクリーンの情報はクラウドに保存して共有、もしくはScreenの構成情報をファイル形式でローカルに保存してファイルで共有・インポートする2通りの方法があります。 ドラッグ&ドロップ 1stからの特徴的な機能で、ドラッグ&ドロップで手軽にダッシュボードを構成できます。 この機能を成立させるには、ドラッグ元とターゲットとなるダッシュボードエリアを同時に確保する必要があります。 選択するリストを表示して、そこからダッシュボードへドラッグ&ドロップする動きです。 左サイドを設定系のエリアとして設計し、フッター(下部)エリアから選択もしくはドラッグ&ドロップするUIとしました。 マウスで操作できない環境などではドラッグ&ドロップの操作が難しい場合があり、パネルをクリックすることでデータを配置することも可能にしました。 全体のコンセプト デザインする上でのお題 これまで述べた機能を成立させるUIを作ることの他に、意識していたことは以下の3点です。 未来感のあるデザイン ダークな背景 フォント 具体的には、SFの世界で表現されるようなUIをモチーフにしたいと考えました。 この時期、SF映画をずいぶん沢山観ました。 画面に奥行きとレイヤー感が欲しくて、初期の頃はデータを重ねるデザインなども検討しましたが、実現性とのバランスで過度なレイヤー表現はなくし、グリッド基調とした現在のデザインになりました。 重ねる感じを実現しているもので、動画や地図パーツを背景レイヤーに全面表示する機能があり、データをオーバーレイさせるとデモ映えしますので、ぜひ試してみてください。 実現したいことは、 データを格好良く見せる ことでした。 その動機は主に製造業のメーカー研究開発部門の方々に、格好良いツールを使って仕事してほしいというものでした。 この時点でのペルソナでもありましたが、30〜40代のデータを扱う仕事の方々には幼少時代に見たSF映画やアニメの世界で見たデータの格好良さに憧れる何かがあります(自分自身含む)。 タスクベースなWindowsネイティブの業務ツールが主流な現場には、コレで仕事できるの?というくらいギャップがあったようですが、熱心なファンになってくれたお客様も多くいました。 ダークな背景 2020年現在、各OSやアプリケーション、Webサイトなんかもダークモードがかなり定着してきました。 VM2Mは2014年当初よりダークモードです。 aptpodコーポレートカラーもブラックです(体制的な意味ではなく…)。 データを見る上で、黒い背景というのは目が疲れにくいとか、コントラスト的に視認性が上がるとか、色々な説があります。 また、たいして効果はない等の研究結果もあります。 wired.jp 上のリンク記事からの引用になりますが、理由の大半はこれです。 ダークモードが人気を集める大きな要因は、その美しさにある。「ナイトモードのTwitterは、普通の表示と比べて1,000パーセント、クールだ」という、あるTwitterユーザーの感想は、ダークモードに対するネットユーザーの代表的なリアクションだろう。 フォント フォントはUIの雰囲気を決める上でとても重要で、日英ともにAXISフォントをベースフォントに採用しています。 未来感を出す上でダークな背景と合わせて、柔らかさと冷たさをもつAXISフォントをベースとすることで、キレのある統一感を出しています。 高速に変化する数字データについては、 以前の記事 で触れています。 失敗 ここまで振り返ると、答えを持っていてスムーズにデザインしたように見えるかもしれません。 当然ながらそんなことはなく、試行錯誤の繰り返しでした。 仮デザインの実現可能性についてCTO kaji 及びフロントエンドエンジニアのshirokaneからすぐにフィードバックをもらえたので、大変助かりました。 振り返ってみて、その当時の環境ではベストを尽くしたつもりですが、失敗だったなと思う点がいくつかあります。 操作系 2015年当時のトレンドとしてPCのタブレット化を見越して、フッターにメインの操作系を配置するデザインを採用しました(VM2Mの対応ブラウザは、当初よりHTML5のアプリケーションとしての性能を限界まで生かしてチューニングする必要があり、PC向けのGoogle Chrome限定としています)。 タブレットPCで操作することを想定した場合、下部にナビゲーションのようなボタンを配置するUIは、大画面で見ても手元のタブレットでも成立しているように思いました。しかし、現在そこまでタブレットPCも流行らず、横にスクロールする動きはPCブラウザで操作するのには相性が悪く、操作し辛い点があると思っています。 横に表示されるリストはタッチディスプレイやタッチパネルでスワイプするような動きならスムーズでも、PCブラウザでの表現は難しいな、と学びました。 また、横型はリストとしての表示の一覧性も悪く、計測の一覧に関しては横並びと縦並びの一覧画面を切り替えるUIを設けていましたが、横並びのものはメンテナンスしていく都合もあり削除し、縦のみとなりました。 初期のフッター(下部ボタン)のデザイン 現在下部にある3つのボタンの内、計測の一覧画面だけ押下後のリスト表示の形式が違っていて、全体の操作統一感的にも最初の設計で躓いていたなと思います。 現在のフッター Visual Parts 可視化するためのパーツを初期にまとめてデザインしました。 このへんは正直えいやっと作ってしまったので、今ならもう少し調査とか、ヒアリングに時間をかけてやるべきだった…と思っています。 当時は自動車メーカーの顧客がほとんどで、そこへの意識が強かったと思っています。 もう少しデータ可視化視点で汎用的なパーツを用意できれば良かったと思っています。 パーツは現在も拡充していますが、初期のコンセプトに引きずられる点もあり、開発者に負担がかかってしまっているのが現状です。 ワークフローを作りきれなかった 開発当初は展示会イベントやデモンストレーションとしてインパクトのある可視化ダッシュボードから作っていきました。 おかげさまで好評でもあり、ダッシュボードの機能改善やカスタム対応に追われました。 しかし実際に使おうとすると、データを可視化する前に必要な作業があります。 エッジ(ユーザーやデバイス)の管理や権限設定など、コンフィグレーションツールが整っていませんでした。 一部の必要なコンフィグ機能をVM2MのUIに持たせてしまったので、機能の改善スピードが落ちる結果になりました。 今後 intdashユーティリティ 前章の失敗を課題として、Webアプリケーションとして保守しやすい形で機能ブロックに分けてintdashのユーティリティ開発が進んでいます。 エッジの状態一覧や、管理機能も近く正式にリリースされる予定です。 計測の状態(データの回収状況など)もAPIの拡張に伴って、Data Visualizerの計測一覧(Stored Data)に反映してバージョンアップを重ねてきましたが、計測の管理ツールとして切り分けて開発中です。 デザイン的にもより実用的な形を実現したいと思っています。 VM2M Data Visualizer コンセプトを一貫したデザインでデータを見た時に感動してくれるお客様がいる、ということは少なからずaptpodの魅力に寄与していると思っています。 パフォーマンス的にブラウザの限界に挑むような全部入りの汎用ツールであるからこそ伝わる魅力があると思っています。 こちらも改良を重ね、近く新しい動きがアナウンスされる予定です。 技術的にもトレンドサイクルの早いWebアプリケーションの世界で、2015〜2016年に設計開始したものが2020年の現在に現役で魅力的に機能しているのは、実装面でチューニングを続けてくれているエンジニアのおかげです。 おわりに リクエストのあった営業サイドの要望に沿った内容になったどうかは自信がありませんが、デザイン視点でVM2Mのコンセプトと歴史を振り返る形になりました。 今作っているもの、これから作るものを考える上でも考えが整理されたので、機会をいただけて感謝しております。 弊社では今後ウェビナー形式や、オンラインでのデモを企画しております。 Data VisualizerをはじめとしたVM2M アプリケーションに触れていただける機会が増えるかと思います。 少しでもご興味を持っていただけましたら、お問い合わせのほどよろしくお願いいたします。 お問い合わせはこちら デザイン室の求人 グラフィックデザインに自信のある、弊社の事業や製品に興味のあるデザイナーを募集しています。 UX/UIは勉強していけば身についていくし、デザイナーがひとりでやるものでもないと思っています。 以下から応募いただければ幸いです。 aptpod Recruit デザイナー デザイン室の職務範囲については 前回の記事 をご参照ください。 応募はためらうけれど、話を聞いてみたいという方がいれば、 お問い合わせ からデザイン室tetsu宛に連絡ください。 昨今のご時世でリモートワーク中ですので、オンラインで気軽に面談させていただければ嬉しいです!
アバター
はじめに 先進技術調査グループのせとです。本ブログでは、Apache MXNetを用いてfastaiで実装されている実践的な関数を真似てみた結果を紹介します。この試みのゴールは、完全一致の結果を目指すのではなく同じような傾向を得られるかを目指したものになります。完全一致を目指したいところですが、各フレームワークで用意しているモデルの構造が少し違ったり、各関数の計算方法が異なるので結果が等しくなりませんでした。もちろん、他方に併せて関数を自作すればほとんど一致する結果を得ることができますが実装のコストが高かったため、今回は行いませんでした。 モチベーション 弊社のプロジェクトでAI部分を Amazon SageMaker (以下、SageMaker)を使って実装したい要望がありました。しかし、プロジェクトで利用していたフレームワークは fastai であるために簡単にSageMaker上で実行できないことがわかりました。この課題を解決するために、はじめは単純な学習を行えば達成できると思っていたのですが、実際にためしたところfastaiで達成した精度を再現できませんでした。このため、ファーストステップとしてfastaiで用いた関数をSageMakerのベースに使われているApache MXNetで真似て精度を再現を行えるかを試みました。 Deep Learningライブラリの説明 ここでは利用したDeep Learningライブラリを簡単に説明します。 fastai fastaiは、 PyTorch をベースにしたDeep Learningのフレームワークです。特徴は、実験的に良かった内容に関する論文の成果を実装して使えるようにしているところです。良いノウハウを簡単に利用できて実戦的に使える良いフレームワークだと思います。 Apache MXNet Apache MXNet(以下、mxnet)はfastaiと同様のDeep Learningのフレームワークで、fastaiとの違いは自作関数を書くような低レベルな書き方や、gluonをつかった高レベルな書き方など柔軟なフレームワークです。 MXNet とは | AWS にあるように、SageMakerに利用されているフレームワークです。 実装した機能 プロジェクトで利用した以下の2つの機能をmxnetで実装しました。 learning rate finder fit one cycle 以下にそれぞれの機能の概要を説明します。 learning rate finder Deep Learningのモデル学習では、色々なハイパーパラメータ(bach size、weight decay、learning rate、momentumなど)を設定する必要があります。不適切なパラメータを用いると、学習しない・収束に時間がかかるなどが起こるため、適切なパラメータを探す必要があります。経験則や一般的に良いと言われている値などを用いて仮決めしたあとに、学習の様子をみて微調整して決定することが多いです。lerning rate finderの機能は、良いlerning rateを決めるためにある範囲内のlearning rateごとにlossを算出します。この結果のlossを見て適切なlearning rateを決定します。詳しい説明は ここ に書かれており、この手法はこの 論文 で提案されています。 fit one cycle Deep Learningのモデル学習では、一般的には学習が完了するまでに時間がかかります。この収束を早くするための工夫がone cycle training機能になります。この機能は、設定したlearning rateを基準に基準より小さいlearning rateから学習を開始しイテレーションのたび大きくします。基準まで到達した後、下限に設定したlearning rateまで段々と小さくします。この手法はこの 論文 で提案されています。 実験条件 実行環境 OS: Ubunt18.04(docker on CentOS7) CPU: Intel Core i7-6850K GPU: GeForce 1080Ti Memory: 63GB nvidia-docker 学習条件 dataset CIFAR10 画像サイズ 224x224x3 ※もともと32x32x3ですが、アーキテクチャを変えたくなかったため224にアップサンプリングしています。推論にとってはあまり必要のない行為です。 モデル resnet50 + cnn learner module ※cnn learner moduleはfastaiの関数で生成される層のことです。 各パラメータ epochs: 3 batch size: 32 データ拡張 fastai: get_transform{max_rotate=5.}で設定できる処理 実験 learning rate finder関数の比較 fastaiでlearning rate finder関数を実行した結果は下図のとおりです。 fastai: learning rate finder mxnetで模擬した結果は下図のとおりです。 mxnet: learning rate finder 上図を比較すると、learning rateごとのlossが似たような形になっていることが見て取れます。このため、関数の実装ができたとしました。 fit one cycle関数の比較 fastaiのfit one cycle関数を実行した結果が下表のとおりです。 fastai: fit one cycle mxnetで模擬した結果は下表のとおりです。 mxnet: fit one cycle 上図を比較すると、1エポックと2エポックでaccuracyとvalid-accが似たような傾向になっています。このため、この関数も実装ができたとしました。 まとめ 上記結果から、fastaiのlearning rate finder関数とfit one cycle関数をmxnetで実現することができました。これらのモジュールを用いてSageMakerへ投げる用のスクリプトを書けば学習・デプロイが可能になると思います。 今回は、プロジェクト起因の課題から手探りで思ったより苦労しましたが、学びが多くありました。fastaiの関数を理解するためにドキュメントや実装を見てみましたが、細かいテクニックなど(バッチ正規化部分だけはフリーズしないなど)が隠蔽化されており、使う側は何も意識しなくて良いですが、他のフレームワークで似たようなことをしようと思うと表面だけ真似ても難しいことがわかりました。フレームワークを自由に選択できるのであればfastaiはベターな結果を短時間で得られるフレームワークだと思いました。 一方、mxnetは低レベル・高レベルの関数が容易されておりかなり簡易に実装を書けることがわかりました。使い方はPytorchに似た部分もあり複数のフレームワークを扱ったことがあればそこまで苦労しないと思います。しかし、mxnet独自の部分はドキュメントや記事などが少なく労力がかかったので、コミュニティがもっと活発になってほしいなと思っています。 今後は、色々な取り組みがある中で、mxnetベースの画像に特化した GluonCV Toolkit やSageMakerの機能などの使ってみた体験を紹介していきたいと思っています。
アバター
検証のアプローチ 実際にためしてみる 使用するデータをさがす 音声からスペクトログラムに表現する データセットを作成する Amazon SageMakerでトレーニングする ハイパーパラメータ調整ジョブを使用して精度向上を試みる 実際にモデルを使用して推論してみる まとめ 最後に みなさまこんにちは。先進技術調査グループのキシダです。私自身は4つめの記事投稿となりました。ネタ切れ感が否めないですが、前回に引き続き音声データにまつわるテーマをご紹介したいと思います。 さて突然ですが、最近AWSが展開している注目の機械学習サービス「 Amazon SageMaker 」に関連した記事をよく見かけるようになりました。さらには今年の初旬あたりに Amazon SageMaker Studio など多数のサービスが発表され、より盛り上がりをみせています。 一方で、私自身は「音声解析」系の技術検証をひっそり行っており、ふと「今話題となっているAmazon SageMakerで音声分類アルゴリズムがサクッと作れたりしないかな?」と思い立ったので、実際に試してみました。 検証のアプローチ 本検証には複数のステップを盛り込んでいるので、以下のアプローチに関心がある方はそのまま読み進めていただければと思います。 モデルのアルゴリズム部分は「組み込み(build-in)アルゴリズム」を使用する Amazon SageMakerには「組み込み(build-in)アルゴリズム」と言われる、実装されたモデルのアルゴリズムが数種類あります。こちらを使用するとモデルのアルゴリズム部分を実装することなく手軽に試せます。 docs.aws.amazon.com 今回は「イメージ分類アルゴリズム」を使用します。 音声は「スペクトログラム」に変換し画像分類にかける 今回は音声の分類機能を作りたいため、音声の特徴量を画像で表現し、その画像を画像分類モデルにかけてみたいと思います。 音声の特徴量を画像で表現するものとして、「スペクトログラム」という形式がよく用いられるのでこちらを使用します。スペクトログラムとは、3次元(時間、周波数、音圧)の特徴量をグラフで表現するもので、視覚的に音声の特徴を捉えることが可能です。 スペクトログラムの例 音声からスペクトログラムに変換して分類するアプローチは以下を参考にしています。 qiita.com Amazon SageMakerのハイパーパラメータ調整ジョブを使用して精度を向上させる 「ハイパーパラメータ調整ジョブ」とは、指定の範囲内で多数のハイパーパラメータを自動的に作成し、複数のトレーニングジョブを実行してくれるジョブです。すべてのジョブが完了すると、指定した指標に対して最も成績がよかったモデルを採用し、結果をレポートしてくれます。 自動モデル調整の実行 - Amazon SageMaker 私自身こちらを試したことがなかったので、これを機会にこのサービスも試してみました。 実際にためしてみる 使用するデータをさがす 今回はユースケース自体を固めず、適当なサンプルデータで検証します。音声分類でよく使用されているサンプルである、 ESC-50 という50種類の音声データを使用します。 GitHub - karolpiczak/ESC-50: ESC-50: Dataset for Environmental Sound Classification 音声からスペクトログラムに表現する まずは音声を画像分類アルゴリズムに与えるために、スペクトログラムに変換していきます。 変換を行うためのライブラリもいくつかあるのですが、今回は librosa というライブラリを使います。 # change wave data to stft def calculate_sp (x, n_fft= 512 , hop_length= 256 ): stft = librosa.stft(x, n_fft=n_fft, hop_length=hop_length) sp = librosa.amplitude_to_db(np.abs(stft)) return sp # display wave in spectrogram def show_sp (sp, fs, hop_length): librosa.display.specshow(sp, hop_length= None ) すると、以下のようにスペクトログラムが出力されます。 データセットを作成する 変換するだけだと簡単なのですが、サンプルのデータをそのまま使うと2000データほどしかないので、トレーニングデータをもう少し増やします。サンプル数2000枚のうち、500枚をテストデータ、1500枚を訓練データに分け、訓練データをさらに以下のように加工することで6000枚に増やします。 元のデータのスペクトログラム ホワイトノイズをかける #data augmentation: add white noise def add_white_noise (x, rate= 0.002 ): return x + rate*np.random.randn( len (x)) フレームをずらす # data augmentation: shift sound in timeframe def shift_sound (x, rate= 2 ): return np.roll(x, int ( len (x)//rate)) 伸縮する # data augmentation: stretch sound def stretch_sound (x, rate= 1.1 ): input_length = len (x) x = librosa.effects.time_stretch(x, rate) if len (x)>input_length: return x[:input_length] else : return np.pad(x, ( 0 , max ( 0 , input_length - len (x))), "constant" ) これである程度のデータ量を作成することができました。 今回はトレーニング時にS3に保存されているデータセットを参照する方式にするので、上記をS3にアップロードして完了です。 次に上記の画像に対して、正解データ(メタ情報)を示したファイルを作成します。 音声に紐づくclassについては、以下のcsvファイルに記載されているのでこちらを参照します。 ESC-50/esc50.csv at master · karolpiczak/ESC-50 · GitHub Amazon SageMakerで扱うメタ情報のファイル形式の1つに、 manifest 形式 という形式があります。今回はこのフォーマットに従い、上記のcsvファイルを manifest ファイルに変換しておきます。 Input Data - Amazon SageMaker 作成したmanifestファイルの一部を以下に記載しておきます。 {"source-ref":"s3://sagemaker-audio-classification/rawdata/ESC-50/validation/2-122104-B-0-0.jpg","class":"0","class-metadata":{"class-name":"dog"}} {"source-ref":"s3://sagemaker-audio-classification/rawdata/ESC-50/validation/4-183992-A-0-0.jpg","class":"0","class-metadata":{"class-name":"dog"}} {"source-ref":"s3://sagemaker-audio-classification/rawdata/ESC-50/validation/4-164021-A-1-0.jpg","class":"1","class-metadata":{"class-name":"rooster"}} {"source-ref":"s3://sagemaker-audio-classification/rawdata/ESC-50/validation/4-164064-B-1-0.jpg","class":"1","class-metadata":{"class-name":"rooster"}} こちらもS3にアップロードしておきます。 Amazon SageMakerでトレーニングする それでは、実際にトレーニングを行っていきます。 Amazon SageMaker Python SDK を使用してトレーニングを進めていきます。 まずは 画像分類アルゴリズムを表す image-classification のコンテナイメージを取得します。 from sagemaker.amazon.amazon_estimator import get_image_uri training_image = get_image_uri(sess.boto_region_name, 'image-classification' , repo_version= "latest" ) Estimator を生成して、hyperparameterを設定します。 ic = sagemaker.estimator.Estimator(training_image, role, train_instance_count= 1 , train_instance_type= 'ml.p2.xlarge' , train_volume_size = 50 , train_max_run = 360000 , input_mode= 'Pipe' , output_path=s3_output_location, sagemaker_session=sess) ic.set_hyperparameters( num_layers= 50 , image_shape = "3,224,224" , num_classes= 50 , num_training_samples= 6000 , mini_batch_size= 32 , epochs= 30 , learning_rate= 0.1 , top_k= 2 ) 先程作成したデータを指定し、トレーニングを開始します。 train_data = sagemaker.session.s3_input( s3train, distribution= 'FullyReplicated' , content_type= 'application/x-recordio' , record_wrapping= 'RecordIO' , s3_data_type= 'AugmentedManifestFile' , attribute_names=[ 'source-ref' , 'class' ], shuffle_config= sagemaker.session.ShuffleConfig(seed= 1234 ) ) validation_data = sagemaker.session.s3_input( s3validation, distribution= 'FullyReplicated' , content_type= 'application/x-recordio' , record_wrapping= 'RecordIO' , s3_data_type= 'AugmentedManifestFile' , attribute_names=[ 'source-ref' , 'class' ] ) data_channels = { 'train' : train_data, 'validation' : validation_data} ic.fit(inputs=data_channels, logs= False ) 以下のログが出力され、完了したことを確認します。 2020-01-29 07:06:15 Starting - Starting the training job 2020-01-29 07:06:17 Starting - Launching requested ML instances................. 2020-01-29 07:07:48 Starting - Preparing the instances for training................... 2020-01-29 07:09:30 Downloading - Downloading input data.. 2020-01-29 07:09:47 Training - Downloading the training image........... 2020-01-29 07:10:47 Training - Training image download completed. Training in progress............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... 2020-01-29 08:16:27 Uploading - Uploading generated training model... 2020-01-29 08:16:49 Completed - Training job completed コンソール上でも完了していることが確認できます。 しかしながら、ログの結果を見てみると、validationデータに対しての正答率が 0.04 と著しく低く、これでは使い物になりません。 そのため、Amazon SageMakerの「ハイパーパラメータ調整ジョブ」を使用して、より精度の高いモデルの作成にチャレンジしてみます。 ハイパーパラメータ調整ジョブを使用して精度向上を試みる 今までは1通りのパラメータでしか実行していなかったので、このサービスを使って複数のトレーニングジョブを一度に実行したいと思います。 まずはハイパーパラメータの調整範囲を指定します。 hyperparameter_ranges = { 'learning_rate' : sagemaker.parameter.ContinuousParameter(min_value= 0.0001 , max_value= 0.005 , scaling_type= 'Auto' ), 'optimizer' :sagemaker.parameter.CategoricalParameter([ 'sgd' , 'adam' , 'rmsprop' , 'nag' ]), 'mini_batch_size' : sagemaker.parameter.IntegerParameter(min_value= 20 , max_value= 100 , scaling_type= 'Auto' ), } HypterparameterTuner を生成します。 tuner = sagemaker.tuner.HyperparameterTuner( estimator=ic, objective_metric_name= 'validation:accuracy' , hyperparameter_ranges=hyperparameter_ranges, objective_type= 'Maximize' , max_parallel_jobs= 1 , max_jobs= 30 , base_tuning_job_name= 'audio-classification' , tags=[ { 'Key' : 'Project' , 'Value' : 'demo' } ] ) ジョブを開始します。 tuner.fit(inputs=data_channels, logs= False ) ジョブが完了すると、コンソール上の「最善のトレーニングジョブ」から、先程 objective_metric_name で指定した指標に対して最も成績が良く出たトレーニングジョブを確認できます。 右下の赤枠に着目すると、最終的に精度が6割弱まであがったことが確認できました! 実際にモデルを使用して推論してみる せっかくモデルが作成できたので実際に推論させてみます。試しに「拍手」の音をモデルに与えて、その判定結果を見てみましょう。 import json image_key = '{}/training/1-105224-A-22-0.jpg' .format( 'rawdata/ESC-50' ) object = s3.Object(bucket_name, image_key) response = object .get() body = response[ 'Body' ].read() classification.content_type = 'image/jpeg' results = classification.predict(body) detections = json.loads(results) 返ってきた結果を確認します。 import numpy as np max_arg = np.array(detections).argmax() max_score = np.array(detections).max() print ( 'result class: {} score: {}' .format(class_dict[max_arg], max_score) ) 実行すると・・・ result class : clapping score: 0.948319673538208 見事に正解していました!確信度も高い値となっています。 まとめ 今回の検証をまとめると、以下の通りとなりました。 音声データと画像分類モデルの親和性の評価 ☺️ 正解率は60% ☺️ 特に変わった前処理も行わずこの精度が出るため、親和性が高いと言えるのでは Amazon SageMakerの「画像分類アルゴリズム」の使いやすさ ☺️ ベースのトレーニングモデルを使用できるなど、精度を手軽に上げるサポートあり Amazon SageMakerのハイパーパラメータ調整ジョブの使いやすさ ☺️ 自動であらゆる種類のパラメータを組み合わせて検証してくれるのは便利。組み合わせ方のアルゴリズムも指定できる 概ね好感触という結果になりました🎉 最後に 今回は音声データをAmazon SageMakerの組み込みアルゴリズムで自動分類できないか検証してみました。 私自身は音声関連の記事ばかり投稿していますが、先進技術調査グループでは他にもロボティクスや機械学習に関連したテーマなど、様々な技術テーマに沿って調査・検証をすすめております。これから投稿される記事も内容の濃いものになると思うので、引き続きwatchいただければと思います! それでは、ご覧いただきありがとうございました!
アバター
CAN FD完全に理解した — Ryuichiro Ohira (@ryu_ohira) 2020年4月27日 はじめに そもそもCANの1ビットはどうやって決まるのか 物理層 :Physical coding sub-layer (PCS) Bit Timeを構成するSegment Synchronization Segment (Sync_Seg) Propagation Segment (Prop_Seg)) Phase Buffer Segment 1 and 2 (Phase_Seg1 and Phase_Seg2) Synchronization:ノード間のビットタイミング同期 Hard synchronization Resynchronization Transmitter Delay Compensation (TDC) : 伝播遅延補償 おわりに 弊社のソリューションのご紹介 脚注 はじめに ハードウェアチームのおおひらです。 本稿では車載の制御通信バス規格として一般的なController Area Network (CAN)の物理層について、その上位側を中心に説明します。 一般的にCAN/CAN FDの仕様について調査するとフレームの種類や構造などのデータリンク層の情報が記載されたドキュメントを目にすることが多いと思います。過去エントリとしては弊社Embeddedチームの久保田の記事( CAN FDことはじめ )だったり、Google検索して出てくるところだと こちら や こちら など。または、Vector社の 『はじめてのCAN/CAN FD』 もしかり。 弊社アプトポッドでも、モバイル回線を利用した遠隔車両データ計測を主たるユースケースとして intdashプラットフォーム のCAN FD対応が進んでおり、改めてハードウェア担当として物理層の理解をすべく規格を調査した次第です。 なお用語に関してはCANの規格書 (ISO 11898-1 1 および BoschのCAN FD仕様書 2 )を参照できるよう、過度に日本語翻訳していません。毎度のことながら、認識に誤りがありましたらご指摘いただけると有難いです。 そもそもCANの1ビットはどうやって決まるのか 物理層 :Physical coding sub-layer (PCS) 普段の会話で「CANフレームのIDは11bitで~」などと言うことがあるかと思いますが、そのビットの0 or 1を判別する仕組みがCANのPhysical layer (PL)の中のPhysical coding sub-layer (PCS) にあります。下記はCANの規格書をもとに作成したOSI参照層とCANのレイヤー構造の比較図であり、CAN FDは ISO 11898-1:2015で規格化されています。 ※ 因みにISO 11898-2はHigh-Speed CAN (~1Mbps) の物理層、ISO 11898-3は商用車や産業機械・重機向けのLow-Speed CAN (~125kbps) の物理層(主に電気的な仕様)の規格 上の図中の黄色でハイライトされた層が本稿で説明するPCSで、役割は以下の2点です。 Bit encoding / decoding Synchronization 具体的には、CANノード間の同期・伝播遅延の補償・Sample Point (ビットの0,1判断)の位置決めなどの機能が、1ビットあたりの時間 : Bit Time を Time Quantum (tq) という時間単位でサンプリングして動作するロジックで実現されます。 例えばCAN FDを想定して考えてみましょう。1つのCANフレームの中にNominal Bit Rate (NBR)とData Bit Rate (DBR)の2種類の通信速度が存在します。BoschのCAN FDの文献 3 では実車環境を考慮した現実的な値としてNBR=500 kbps 4 、DBR=2 Mbps 5 が記載されていますので、これに従うと逆数で1ビットあたりの時間(Nominal Bit Time (NBT), Data Bit Time (DBT))はそれぞれ 2 us、0.5 usと計算できます。 他方、上記のBit TimeをサンプリングするためのTime Quantum (tq)の周期は、個々のCANノード内のデバイス(CANコントローラ)に供給されるシステムクロックの周波数に制約されて決まります。CANコントローラがMCUやSoC内蔵のペリフェラルとして統合されている場合 6 はそのペリフェラルに供給するクロック周波数に相当し、単独ICタイプの製品 7 では外付けの水晶振動子とチップ内のPre-Scaler (分周器)の設定に依存します。多くのCAN FD製品では40 MHzのシステムクロックを用いて1 tq = 25 nsでサンプリングすることが推奨されているようです。 ここまでのCAN FDのフレームの構成要素を分解しつつ 鳥の目 to 虫の目 的に示したものが下の図です。フレーム→フィールド→ビットタイムの順に要素が細かい時間単位になります。(各ビットの役割などは他記事をご参照下さい) ビットが0(ドミナント)か、1(リセッシブ)かの判断は上図の赤字で示したSample pointで行われます。 Sample pointのtq値は上位ソフトウェアから設定可能ですが、これはあくまで初期値であり、CANノード間のクロック周波数偏差や信号伝播遅延の影響を考慮して同期のロバスト性を高めるために自動的に位置をずらす仕組みが実装されています。次項ではBit Timeの単位で正確な同期を実現するためのSegmentの役割について記載します。 Bit Timeを構成するSegment Synchronization Segment (Sync_Seg) ノード間の同期をとるために定義されており、規格上1 tqと決められています。信号レベルのエッジがこの期間内に存在することが期待されます。 Propagation Segment (Prop_Seg)) ノード間の物理的な電気信号遅延の補償のために定義されています。最も伝播遅延の大きいCANノード間において、信号が行って返ってくるまでの合計時間よりも大きなtqを設定する必要があります。 Phase Buffer Segment 1 and 2 (Phase_Seg1 and Phase_Seg2) 後述する Resynchronization (再同期)のロジックにおいて位相補償を行います。初期値となるtqを設定した上で、Resynchronizationのために時間的に伸び縮みします。このtqの限度値は別途 Synchronization Jump Width (SJW) という値で制約することができます。 CANコントローラの実装上、Phase_Seg1のtq数はProp_Segのtq数と合算して設定してよいことになっています。また、1 Bit Timing期間の総tq数は、 Classical CANの場合8~25 tq、CAN FDの場合はNBTで8~80 tq , DBTで5~25 tq と規定されています。 Synchronization:ノード間のビットタイミング同期 ノード間の同期機能は、1 tq 時間内にステートマシン動作として実現され、1 Bit Timeあたり1回だけ動作することが規定されています。 Hard synchronization と Resynchronization の2種類の同期方法が規定されています。 Hard synchronization CANフレーム間にリセッシブからドミナントへの信号変化のエッジ(つまりSOFビット)を検知したとき、Bit Timeをリセットしてそのエッジの箇所をSync_Segと定義します。Bit Timeのtqカウンタのリセット機能と言い換えることもできます。この同期手段はSJWのtq数の制約を受けません。 Resynchronization Sync_Segの期間内に信号のエッジを検出できなかった場合にタイミングの再同期を行います。Phase_Seg1 or 2 の伸張tq数はSJWで制限されます。 信号エッジの検出位置 Phase Error定義 再同期の実現方法 Sync_Segのtq期間内 e=0 - Sync_SegとSample Pointの間 e>0 Phase_Seg1を伸ばす Sample Point と次のBit TimeのSync_Segの間 e<0 Phase_Seg2を縮める Transmitter Delay Compensation (TDC) : 伝播遅延補償 CAN FDの高データレートで2~5Mbps 程度になってくると、CANネットワーク上の電気的な遅延だけではなく、CANコントローラとCANトランシーバ 8 の間で生じる数十~数百 ns オーダの遅延量に対する補償も必要となります。例えばCANコントローラが出力した信号がトランシーバのTxピンに伝送され、トランシーバIC内部の送信回路と受信回路を通ってRxピン経由で再度CANコントローラに戻ってくるループバック経路を考えただけでもISO 11898-5 9 では 最大255 ns の遅延が許容されていますので、CAN FDのデータレートを高めていく 10 と全くビットのサンプリングが成り立たないことが分かるかと思います。デバイス単体でのループバックテストすらエラーになってしまいますね。。 ここまでくるとICの設計者的な視点でかなり深い話になってきますので、詳細が気になる方は別途資料 11 をご参照頂くとして、その補償手段としてデータビットレートにおけるビットのサンプリングを遅延させる Secondary Sample Point (SSP) 機能が実装されています。これはアービトレーションフェーズにおけるFDFビットとresビット間のリセッシブからドミナントに信号変化するエッジのループバック信号をCANコントローラが計測し、Tx(送信)とRx(受信)の時間差分を用いてSSPの位置を決めるものです。0~63 tq の範囲でSample Pointを遅延させてSSPを実現することが規定されています。 おわりに 物理層の上位側に相当するPCSの仕組みをまとめてみました。規格書をじっくり読むと高データレート・ロバスト・低コストという要素を両立させるべく色々な工夫がされていることが分かって面白いものですね。 次回はBoschのOriginal CAN FD仕様とISO仕様との違いとか、ISO 11898-2:2016のWake Up Pattern (WUP)など、もう少しCAN FD規格を深堀りしてみたいと思います。 ※そうこうしているうちに CAN XL という更に高データレートな規格も議論されていたりしますが… 弊社のソリューションのご紹介 弊社では、CAN/CAN FDに対応した、自動車向けの遠隔計測バンドルパッケージ商品を提供しています。 www.aptpod.co.jp こちらは、 CAN/CAN FDデータの収集の遠隔化システムをハードウェアからクラウドサービスまでオールインワンパッケージとしてご提供する商品です。 弊社がご用意するエッジコンピュータとその付属品を自動車に取り付けていただくだけで、エッジコンピュータが自動的にCAN/CAN FDデータをクラウド上へ収集し、Webベースのダッシュボードツールから、いつでも、どこからでもデータを確認いただけます。 CANやCAN FDの遠隔計測ソリューションにご関心をお持ちの方は、ぜひ一度アプトポッドまでご相談下さい。 www.aptpod.co.jp ちなみに、遠隔適合に対応した商品もございます。こちらも併せてご検討いただけますと幸いです。 www.aptpod.co.jp 脚注 https://www.iso.org/standard/63648.html ↩ https://can-newsletter.org/assets/files/ttmedia/raw/e5740b7b5781b8960f55efcc2b93edf8.pdf ↩ https://www.bosch-semiconductors.com/media/ip_modules/pdf_2/papers/icc14_2013_paper_hartwich_1.pdf ↩ 米国自動車技術会(SAE) J2284で規定された値で、現在の乗用車で採用されている一般的な定格ビットレート ↩ 実車における電気的な制約、EMC(電磁波の不要輻射)、温度変動要件などを考慮するとこのあたりの速度が適当らしい ↩ ルネサス, NXP, STMicroelectronics, Cypressなど各社が多くのSystem on a Chip (SoC)を供給している ↩ MicrochipのMCP251xxFDシリーズ、Texas InstrumentsやInfineon Technologiesの製品ラインナップにあるController & Transceiverを統合したSystem Bases Chipなど ↩ 電気信号のシングル-差動変換・温度補償・CANバスの線路の短絡/過電圧保護やSleep/Wake upの状態管理をする末端ICの一般名称 ↩ High-speed CAN (1Mbps) の低消費電力の拡張規格 ↩ CAN FDのデータレートの上限は規定がなく、ECUのリプログラミングなどの特定用途では5~8 Mbpsの速度も想定されている ↩ https://www.infineon.com/dgdl/Infineon-IFX_The_Physical_Layer_in_the_CAN_FD_world_published-WP-v02_00-EN.pdf?fileId=5546d462525dbac40152a66f6a440d63 ↩
アバター
先端技術調査グループの大久保です。 弊社では現在、クラウド上でROSの開発が行える AWS RoboMaker を利用しており、GazeboシミュレーションもRoboMakerを使って行っています。当ブログでも、RoboMakerを使ったシミュレーションを以前取り上げています。 tech.aptpod.co.jp 現在は、シミュレーション上のロボットにdepthカメラを取り付け、depth情報を収集できるようにしています。 このdepth情報ですが、32bit浮動小数点数のバイナリ列のため、そのままでは可視化して確認することができません。ROS用のツールを使って可視化することはできますが、弊社の Visual M2M なら、ROSトピックとして流れる画像をネットワーク越しに確認することができるため、これを利用します。その時必要になるのは、depth情報のROSトピックをjpegに変換して、それを別のROSトピックに流すノードとなります。 ROSのノードを記述するためにはC++かPythonを使うのが一般的ですが、Rustでも記述することができます。そこで今回は、Rustを使ってノードを書いてみます。 ビルドの設定 Robomaker上のプロジェクトはロボット本体へデプロイするコードを集めたworkspaceと、シミュレーションのためのworkspaceで構成されていますが、今回はロボット用のworkspaceである robot_ws にRust製のノードを追加します。depth情報をjpegにするため、depth2jpegという名前を付けます。次のようにROSパッケージを追加します。 cd robot_ws/src catkin_create_pkg depth2jpeg std_msgs sensor_msgs cd depth2jpeg mkdir src && cd src cargo new depth2jpeg Robomakerを使う場合、 CMakeLists.txt に colcon build でCargoを呼び出してビルドするよう設定する必要があります。また、 colcon bundle で実行ファイル等の必要なアセットがバンドルされるよう設定する必要もあります。そのために、自動生成された CMakeLists.txt に次の内容を追記します。設定の中にある各パスの指定はプロジェクトの構成に応じてうまく変えてやります。 # ビルド用の設定 # ビルド時にcargoが呼び出されるようにする。Cargo.tomlのパスを指定 add_custom_target(depth2jpeg ALL COMMAND cargo build --release --manifest-path ../../src/depth2jpeg/src/depth2jpeg/Cargo.toml ) # launchディレクトリをバンドルするための設定 # launchディレクトリにはlaunchファイルを入れる install(DIRECTORY launch DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} ) # cargoのビルドで生成した実行ファイルをバンドルするための設定 install(PROGRAMS src/depth2jpeg/target/release/depth2jpeg DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} ) RustでROSのノードを記述するには、 rosrust を用います。その他の必要になるクレートを含めると、 Cargo.toml には以下のように依存関係を記述しておきます。 [dependencies] rosrust = "0.9" rosrust_msg = "0.1" image = "0.23" byteorder = "1" 前述の通りrosrustを入れておきます。rosrust_msgは、入れておくことでビルド時にその環境で定義されているROSメッセージを自動でインポートしてくれる便利なクレートです。imageはJPEGへのエンコード用、byteorderはバイナリ形式のdepthを読むのに使います。 Rustでノードを書いていく depth情報が入っている /depthcam/depth/image_raw という名前のトピックを受け取り、それをjpegに変換したら /depthcam/depth/jpeg というトピックで送出するもととします。この場合、main関数は以下のように記述します。 use byteorder :: {ByteOrder, LittleEndian}; use rosrust; use rosrust_msg :: sensor_msgs :: {CompressedImage, Image}; use std :: sync :: Arc; fn main () { // depth2jpegという名前でノードを初期化 rosrust :: init ( "depth2jpeg" ); // 送出用のトピックを開く。Arcで囲む let p = Arc :: new ( rosrust :: publish ( "/depthcam/depth/jpeg" , 2 ). unwrap ()); // 受信時用のコールバックを登録する let _subscriber_info = rosrust :: subscribe ( "/depthcam/depth/image_raw" , 2 , move | img: Image | { p. send ( depth2jpeg (img)). unwrap (); }) . unwrap (); // 終了のシグナルを受信するまで待つ rosrust :: spin (); } やっていることは単純で、トピックを2つ開き、コールバックを登録するだけです。受け取るトピックの型には Image 、送出するトピックの型は CompressedImage です。この変換を行うのはdepth2jpeg関数です。この関数は以下のように記述します。 fn depth2jpeg (img: Image) -> CompressedImage { let width = img.width; let height = img.height; let raw_data = & img.data; // 32FC1 littleendianをVecへデコード let mut i = 0usize ; let pixels: Vec < f32 > = std :: iter :: from_fn ( move || { if i < (width * height) as usize { let value = LittleEndian :: read_f32 ( & raw_data[(i * 4 )..(i * 4 + 4 )]); i += 1 ; Some (value) } else { None } }) . collect (); // ピクセルのうち、深度が最大のものを探す let mut max = 0.0 ; for pixel in & pixels { if * pixel > max { max = * pixel; } } // RGB24bitのバイト列への変換 let pixels: Vec < u8 > = pixels . iter () . flat_map ( | pixel | { if ! pixel. is_nan () { // 最大深度のところを白として深度をグレースケールに変換する let d = (pixel * 255.0 / max) as u8 ; vec! [d, d, d] } else { // 深度がNaNのときは赤にする vec! [ 255 , 0 , 0 ] } }) . collect (); // jpegにエンコード let mut data = Vec :: new (); let mut encoder = image :: jpeg :: JPEGEncoder :: new ( &mut data); encoder . encode ( & pixels, width, height, image :: ColorType :: Rgb8) . unwrap (); // CompressedImageを作成 CompressedImage { header: img.header, format: "rgb8; jpeg compressed bgr8" . to_owned (), data: data, } } Image.data は Vec<u8> なので、これをエンディアンに注意しつつ Vec<f32> に変換してやります。その後深度の最大値を求め、最大深度のピクセルを白としてグレースケールに変換してやります。Gazeboによるdepthカメラは、ある程度の近距離、もしくは遠距離になると、NaNが格納されるため、そこは赤色とします。 実行 CMakeLists.txt と同じディレクトリ内に launch ディレクトリを作成し、その下に depth2jpeg.launch を作成します。ただ実行ファイルを呼び出すのだけですので以下のようにします。 <launch> <node pkg = "depth2jpeg" name = "depth2jpeg" type = "depth2jpeg" /> </launch> あとはこのlaunchファイルが、robomakerのジョブが立ち上がった時に参照されるように記述してやれば、このノードが立ち上がります。この設定はプロジェクトごとに異なるので割愛します。 jpegで流れているトピックがVisual M2Mで見られるよう設定し、見てみた結果が以下のようになります。 Visual M2M上のdepth表示 画面のほとんどが赤くて測定範囲外であることがわかりますが、それ以外の部分では深度情報が拾えていることがわかります。 Rustを使ってみた感想 Rustを使うとPythonに比べるとバイナリ操作が普通に書けるのがありがたいです。また、RoboMakerを使って開発していると、「修正→ビルド→バンドル→S3にアップロード→シミュレーションジョブの立ち上げ」という一連のプロセスにそれなりの時間がかかります。Pythonだと実行してみるまでエラーが分からないので、コンパイル時にエラーが検出できるのはかなり大きいです。C++でもいいのですが、イテレータ等で洗練した感じに書けたり、jpegエンコーダのようなライブラリも Cargo.toml に設定書くだけで簡単に使えるのはかなり楽です。 こうして見ると、ロボティクス分野でRustを使うポテンシャルは結構大きいのでは、と考えています。そのため、今後もロボティクス分野でのRustの可能性を探っていこうと思います。
アバター
はじめに はじめまして、人事の神前(こうさき)です。 4月1日に入社をして早々に本ブログ記事の執筆をすることとなり戦慄したのですが宜しくお願いします。 改めてにはなりますが、去る4月7日(火)に 緊急事態宣言 が出されました。 緊急事態宣言以前からリモートワークへ切り替えていた企業も多いとは思いますが、これを機に一気に加速したように感じます。 私個人の話でいうと、前職では 某ゲーム会社 に在籍してまして、優先順位的に開発側ではリモートワークの準備や実施がされていたのですが、管理部門でのリモートワーク実施前に退職したこともあり弊社に入社をしてから本格的なリモートワークとなりました (前職のノリで入社初日にフィギュアを持ち込んでデスクに飾ってたら「なんかやべーやつが入社してきたぞ」と若干社内がざわついたことはここだけの秘密) 。 弊社でも2月中頃からリモートワークへの切り替えを推進してまして、現在では ほぼ100%の社員がリモートワークに移行し 、(物理的にどうしても出社しないといけないとしても)出社日も限定して日々業務をすすめています。 さて、そんな状況下ですと日々知見だったり問題だったりがたまっていきます。 そこで今回はネット上にある他社さんのリモートワーク事例やTIPSのご紹介と弊社での取り組み事例をご紹介できればと思います。 本記事がみなさんのリモートワークの一助になれば幸いです。 他社さんでのリモートワーク取り組み事例、TIPS 2010年からリモートワークを段階的に導入されている サイボウズ さんの記事です。 試験的な導入から全社への展開、それに合わせた人事制度の変遷など歴史を感じさせる内容となっています。 記事の最後にはFAQもあり とりわけこれから本格的に導入や運用をしていこうという際にものすごく助けになることうけあいです。 cybozu-remotework.qloba.com さきほどのサイボウズさんとはうってかわって実際に業務をする際の手助けになるのがこちらの freee さんの記事です。 リモートワークの環境を整えることやコミュニケーションを意識して多めにとることの重要さ等 実際にリモートワークで業務をすすめていく際の大事なポイント がわかりやすく書かれています。 developers.freee.co.jp コネヒト さんも同様に実際にリモートワークをすすめるにあたっての注意事項がわかりやすくまとまっています。 チェックリストもある ので環境やツールをそろえるのに非常に参考になりますね。 tech.connehito.com 最後は150名の全社総会をオンラインで実施された グッドパッチ さんの記事です。弊社でも全体会議という全社員が参加する会を設けてますが、こちらの記事では実際の運用ノウハウだけでなく、 いかにオンライン上で会社としての一体感を醸成するか といった点でも非常に参考になります。 https://goodpatch.com/blog/generalmeeting-fy2020half/ goodpatch.com 弊社でのリモートワーク取り組み事例 まず改めてですが現状の弊社のリモートワークにおけるルールについて説明します。 原則自宅勤務を前提とする やむを得ず出社する場合は事前にチームのチャンネルで報告し、マネージャー/部門長は出社の要否を確認 出社の判断に迷う時は所属部門マネージャー/部門長に相談。マネージャーから案件担当者に状況確認したうえで判断 社外との会議は、可能な限りビデオ会議など非対面での実施を推奨し、訪問・来訪を控える 不要不急の出張やイベントへの参加は控える 採用面接は延期または中止することなく、原則ビデオ会議で実施。最終面接については状況に応じて対面での面接も検討 上記が原則ルールとなってまして、特段変わったルールではないと思います。 実際の運用面でいうと、弊社ではSlackをコミュニケーションツールとして利用しているのですが、それぞれが所属する部署のチャンネルで業務開始と業務終了を自己申告しています。 私が所属する管理部での朝の光景 ちなみに、リモートワークなのか休みなのかあるいは何かしらの用事でオフィスに出社している場合などそれぞれのメンバーのステータスをスタンプで表現して視認性をあげています(🏡はリモートワークを表しています)。 また、基本的なコミュニケーションは Slack をベースにしてMTGは Google Meet (本記事執筆中に名称がHangouts Meetから変わりました)もしくは部署やチームによっては Zoom を利用しています。 運用面での工夫 部署やチームのメンバーが常駐するオープンチャンネルを設置 日常的にSlack等でテキストベースのコミュニケーションはとっているものの、雑談をしにくかったり、あるいは口頭でちょっと相談をしたいみたいなことはあるかと思います。私が所属しているチームでは常駐部屋を作成して基本的に業務中はつなぎっぱなしにしています(マイクとカメラはオフ)。 テキストベースのコミュニケーションの場合どうしても非同期になりがち なので、口頭で話したほうがすぐ終わるケースや急ぎで確認したい案件の場合はマイクをオンにして呼び掛けてコミュニケーションをとるようにしています。 実際に呼び掛けをする頻度はそれほどはないのですが、 「そこにだれかがいる」「呼びかければ答えてくれる」 という安心感の効果はかなり大きいように個人的には感じています(感覚的にはオフィスで業務をしているのにわりと近い感覚になります)。 社内のだれでも入れる雑談チャンネルを設置 部署やチーム単位とは別にだれでも入れる雑談部屋を作成して、とくにお昼休憩時や業務終了後にちょっと雑談をしたいなー、みたいな場合はその部屋でコミュニケーションをとっています。そこから発展して 「このままオンライン飲み会しましょうかー」 、みたいなケースも。 Slack上に#remotehackチャンネルを設置 それぞれのチームや部署でたまってくる知見や問題点を共有するチャンネルをSlack上にて作成しています。他社さんでの事例や便利なツールの共有、チーム内でのノウハウを共有できるようにしています。最近の話題は作業用のデスクやイスの話題でした(腰が・・・みたいな話題)。 オンライン飲み会の実施 有志での実施とは別に会社として実施しましょうか、みたいな話をすすめています。(私含めて)弊社ではリモートワークが本格化した後に入社したメンバーが何名かいるため、そうしたメンバーと交流する場をつくれるようにしていきたいという感じです。 制度的な補助 こんな感じでリモートワークの運用をすすめていますが、では全員が平穏無事に何事もなく勤務できているかというともちろんそういうわけではありません。 業務に適したデスクやイスが自宅にない場合や、あっても長時間の勤務で腰や首に負担がかかったり、まだ小さいお子さんがいて自宅ではどうしても業務のみに集中するのは難しいといったようなケースが出てきたり、あるいは今は特に問題がなくとも今後も同様に問題がないまますすむといったことはないでしょう。 弊社ではそうしたケースに対して、不要不急の外出を控えることを大前提として、下記のケースに該当する社員については会社で一定の金銭的な補助を実施しています。 対象者 * 小学生以下のお子さんがいる * 自宅にデスクなどの物理的に適切な環境がない * 在宅勤務はなんとかできるが、長時間厳しい 上記対象者に対してカフェ、ファミレス、ワーキングスペースで作業をした場合に一定額の補助をしています。 (もちろんオフィスに出社しないようにしたからといってこれらの場所で感染をしては本末転倒なので、混雑している場所の利用や、あるいは電車やバスを利用してカフェ等に行くことは 避けて もらっています) まだ明確な会社からの補助としては上記にとどまっていますが、リモートワーク期間が長期に及ぶことでこれから様々な問題がでてくることが予想されるわけですが、会社としては極力社員の健康面に配慮をしつつ、問題解決をなるべくスピード感をもって図っていきたいと思います(リモートワーク用のディスプレイとかイスとかの補助ができるといいなー。 というか家にもディスプレイほしい )。 まとめ 簡単ではありますが他社さんの事例含めたリモートワークの技術(Tipsやノウハウ)を今回ご紹介しました。 ご紹介をした他社さんの事例にもありますが、自身でもリモートワークにシフトしてからは コミュニケーション面がどうしても弱くなってしまう な、というのを痛感しています。立場柄私は現場の皆さんが快適に業務をすすめられるようにサポートをする側ですので、まだ入社して間もなくて右も左もわからない状況ですが皆さんのお力になれるように日々精進していきたいと思います。また、こうしたノウハウや知見は会社内にとどまらず会社間や業界間の垣根を越えて共有できるようになるといいですね。 それではみなさんよいリモートワークライフを!
アバター
先端技術調査グループの南波です。ウイルスは大変な状況ですが、原則自宅勤務となったことで息子2人のお昼寝を眺められる時間が増え、すこしほっこりもしています☺️ さて、今回は最近のお仕事の中で intdashのサーバーに蓄積されているH.264の動画データを解析したい H.264のライセンスはもちろんクリーンに対処したい プロダクト投入時には Amazon ECS なども利用してスケールさせたいので、解析環境はDocker上に用意したい といった課題に対し OpenH264 をDocker上で利用する方法を調査・検証したので、その内容の共有です。 背景 過去の独自ビデオエンコーダの記事 でもご紹介あったように、弊社ではH.264のコーデックで圧縮された動画データを収集・伝送・保存・可視化するために必要となるハードウェア/ソフトウェアの開発にも力を入れています。 となると、もちろん次のステップとしては「その動画データを解析したい!機械学習したい!!」となり、実際に弊チームでもいくつかのテーマで取り組みを行なっています。 しかしいざ「作ったものをサービスとしてみなさまに使っていただこう」という段になると、重要な検討ポイントが浮かんできます。H.264のライセンスの扱いです。(参考: AVC/H.264 Patent Portfolio License Program | MPEG LA : MPEG LA ) 今回は OpenH264 にて配布されているビルド済みバイナリを利用する方法を検討しました。 また、弊社の intdash Analytics Services ではPythonスクリプトからSDKを用いてintdashに保存されている動画データにアクセスできることや、NumPy / Pandas / Tensorflow / PyTorchといった数値演算・機械学習ライブラリと併用することを見据えて、Pythonから動画データを扱うことを目指しました。 intdashアセット図 (免責:ケースバイケースでライセンスへの対応方法は異なるかと思いますので、同様の課題に取り組まれる際は各人で法務部等にご確認ください。当記事によって損害が生じた場合でも当社は責任を負いません) 実施内容 コードは aptpod/openh264-ffmpeg-py です。 Dockerfileの中では ライセンス的にクリーンなH.264動画エンコードのやり方 - Qiita を参考に、記事作成時点で最新のOpenH264バイナリの配置とFFmpegのインストールを行なっています。 Pythonスクリプトでは、サンプルの(Mac上のQuickTime Playerで画面収録した)H.264コーデックの .mov ファイルをフレーム毎にJPEGファイルにデコードし、そのJPEGファイル群をH.264でエンコードし直した .mp4 ファイルを作成しています。 実行時の出力が以下のようになっていることから、エンコードにlibopenh264を利用できていることを確認できました。 re-encode them to a mp4 file using libopenh264: ffmpeg version n4.2.2 Copyright (c) 2000-2019 the FFmpeg developers built with gcc 8 (Debian 8.3.0-6) configuration: --enable-libopenh264 --enable-libmp3lame --enable-libopus --enable-libvorbis --enable-libvpx libavutil 56. 31.100 / 56. 31.100 libavcodec 58. 54.100 / 58. 54.100 libavformat 58. 29.100 / 58. 29.100 libavdevice 58. 8.100 / 58. 8.100 libavfilter 7. 57.100 / 7. 57.100 libswscale 5. 5.100 / 5. 5.100 libswresample 3. 5.100 / 3. 5.100 Input #0, image2, from '/tmp/%05d.jpg': Duration: 00:00:14.44, start: 0.000000, bitrate: N/A Stream #0:0: Video: mjpeg (Baseline), yuvj420p(pc, bt470bg/unknown/unknown), 2052x728 [SAR 1:1 DAR 513:182], 25 fps, 25 tbr, 25 tbn, 25 tbc Stream mapping: Stream #0:0 -> #0:0 (mjpeg (native) -> h264 (libopenh264)) 今後は今回のスクリプトをベースに、中間出力物であるJPEGファイルを機械学習モデルに食わせた結果をオーバーレイしたり、逐次入力されるH.264のユニットを取り扱えるようにしたりなどの要素を追加していき、 過去のお菓子の高速検出システムの記事 のようなことも同等に実現できるようにしていきます 💪
アバター
Webチームの蔵下です。弊社で開発している intdash には、Media Servicesという動画や音声などのメディアデータを扱うサービスがあります。さまざまなカメラに対応できることもあり、 RICOH THETA のような360°カメラで撮影した動画を扱うこともあります。 「 全天球画像 | RICOH THETA 」より引用 ▲360°動画。360°の映像が1つのパノラマ動画内に収められています。歪みを補正して再生するためには、専用の動画Playerが必要になります。 この360°動画をブラウザで再生する360°動画Playerを、JavaScriptの3Dフレームワークである three.js で開発しました。 本記事では、 360°動画Playerを開発するときの実用的なTIPSを紹介します。 360°動画Playerの仕組み 実用TIPS集 TIPS1: three.jsのRenderingとVideo描画でLoop処理を分離する TIPS2: Playerの外からPitch, Yaw, Zoomを操作できるようにする TIPS3: 動作テストはStorybookで実装する おわりに 360°動画Playerの仕組み 360°動画をHTMLのVideoタグで再生しても、上の画像のように歪んだ状態で再生されてしまいます。この歪みを解消するためには3D(three.js)で実装します。詳しいロジックについての解説は記事「 お手軽360°パノラマ制作入門! 」がわかりやすいのでご覧ください! 3D空間に球体を配置する 球体の内側に動画をテクスチャとして貼り付ける 目となるカメラを球体の内側に配置する 配置したカメラの回転角度を垂直方向(Pitch)・水平方向(Yaw)に回転させることで、360°動画内の見たい方向の映像が描画できます。 実用TIPS集 360°動画をthree.jsで表示する方法の解説は、すでに他のブログでわかりやすく解説されているため、 本記事では360°動画を表示した先の実用TIPSを紹介します。 TIPS1: three.jsのRenderingとVideo描画でLoop処理を分離する 使用するカメラやユースケースによって動画のFPSは異なります。すべてのFPSへ対応するために最大FPSで描画処理のタイミングを固定してしまうと、低FPSの動画でムダな描画処理が実行されてしまいます。 動画FPSによって描画処理を間引けばムダな描画はカットできるのですが、そのままthree.jsのRenderingを間引いてしまうと、操作時にカクつきが出て操作性が落ちてしまい本末転倒です。 そこで、three.jsのRenderingとVideo描画でLoop処理を分離し、Video描画用のTextureも VideoTexture から CanvasTexture へ置き換えました。これにより、Video描画を動画FPSの間隔で実行できるようになります。 // three.jsのLoop処理 const tick = () => { renderer.render(scene, camera) } // three.jsのRendering処理開始: requestAnimationFrame renderer.setAnimationLoop(tick) // VideoのFPS const VIDEO_FPS = 10 // Videoの描画用Loop処理 const loopVideo = () => { // VideoをCanvasへ描画する const context = captureCanvas.getContext( '2d' ) context?.drawImage(video, 0, 0) // Rendering時にMaterialを更新する material.map.needsUpdate = true // Videoの描画はsetTimeoutで実行 window .setTimeout(loopVideo, 1000 / VIDEO_FPS) } // 動画の描画処理開始 loopVideo() ※ 複数のrequestAnimationFrameでLoop処理すると、片方の実行タイミングにもう片方が引っ張られてしまったため、setTimeoutで分離しました。 TIPS2: Playerの外からPitch, Yaw, Zoomを操作できるようにする 360°動画Playerのソースコードはネット上に数多く公開されていますが、Playerの外部からボタンなどで操作する方法を解説している記事は多くありませんでした。 360°動画Playerに最低限必要な機能は OrbitControls を使用すると手軽に実装できるのですが、不要な機能も多く、外部から操作しづらいとうこともあり、今回はOrbitControlsのロジックを流用して必要な機能を自前で実装しました。詳しい解説は割愛しますが、次のソースコードは実装箇所の一部です。 // Set Pitch const setPitch = (angle: number) => { // angle: -90 ~ 90 targetPitchAngle = ((angle + 90) * Math.PI) / 180 } // Set Yaw const setYaw = (angle: number) => { // angle: 0 ~ 360 targetYawAngle = ((angle - 180) * Math.PI) / 180 } // Set Zoom const setZoom = (zoom: number) { // zoom: 100 ~ 200 targetZoom = ( this .maxDistance - this .minDistance) * (1 - (zoom - 100) / 100) } // 略... // Update Yaw if (targetYawAngle !== undefined ) { spherical.theta = targetYawAngle + sphericalDelta.theta targetYawAngle = undefined } else { spherical.theta += sphericalDelta.theta } spherical.theta = Math.max( minYawAngle, Math.min(maxYawAngle, spherical.theta), ) // Update Pitch if (targetPitchAngle !== undefined ) { spherical.phi = targetPitchAngle + sphericalDelta.phi targetPitchAngle = undefined } else { spherical.phi += sphericalDelta.phi } spherical.phi = Math.max( minPitchAngle, Math.min(maxPitchAngle, spherical.phi), ) // Update Zoom if (targetZoom !== undefined ) { spherical.radius = targetZoom + sphericalDelta.radius targetZoom = undefined } else { spherical.radius += sphericalDelta.radius } spherical.radius = Math.max( minDistance, Math.min(maxDistance, spherical.radius), ) TIPS3: 動作テストはStorybookで実装する 360°動画Playerを実装するにあたり、動作テストもさまざまなユースケースが考えられました。ゼロから動作テスト用の画面を組むのは実装コストがかかり現実的ではなかったため、 Storybook を使用して実装しました。Storybookを起動するだけで環境が構築できるので、誰でも動作テストができるところもメリットです。 ▲ローカルにある360°動画を実装したPlayerに読み込んだ動作テスト。360°動画(上部Video)とPlayer(下部Canvas)の表示を比較できる。 ▲外部に設置したボタンから360°動画Playerを操作する動作テスト。想定通りの方向を描画できるか確認します。 おわりに アプトポッドでは、intdashのユーザーへより充実した機能を届けるべく、さまざまなアプローチで日々試行錯誤しています。本記事で紹介した360°動画Playerをintdashと組み合わせることで、360°カメラで撮影した映像を遠隔から低遅延で確認できるようになり、より人の目に近い体験を提供できると考えています。 今回、実用的な360°動画Playerを開発するにあたり、ネット上の情報だけでは「もう少し深い機能がほしい!」「かゆいところに手が届かない!」という場面が多々ありました(もちろん情報を発信してくださっている方々には頭が上がりません!)。そこで本記事では、開発する中で用いた実用TIPSを、これから360°動画Playerを実装する皆さんの力になれればという思いで紹介しました。 記事のボリュームの兼ね合いで割愛した部分も多いですが、リクエストいただければ別記事で解説できればと思いますので、お気軽にお問い合わせください!
アバター
はじめに はじめまして。VPoPの岩田です。 弊社は、昨年の8月より開発本部のマネジメント体制を強化し、CTO・VPoE・VPoP から構成される CTO 室体制を敷いております。 aptpod Advent Calendar 2019 では、VPoE 高橋より こんなエモくて素敵な記事 が投稿され、 CTO 梶田からは 1年を総括する記事 が投稿されており、 私も彼らの後に続くべく、遅ればせながら本記事を投稿することにいたしました。 今回の私の記事の題材はというと、ずばり 「アプトポッドってなにをしている会社なの?」 です。 私自身アプトポッドに転職をしてきてから はや3年が経ちますが、実を言うと、今だに身内や友人から 「お前の会社はなにをしている会社なんだ?」 と聞かれることがよくあります。 それもそのはず、アプトポッドの技術領域は、 ハードウェアの自社企画・開発 から、 クラウド環境をフル活用したサーバーアプリ・Webアプリの開発 まで、多岐に渡ります。製品の形態としても、SaaSやPaaS、ミドルウェア製品までラインナップしていますので、一言で なにをしている会社か と説明することは容易なことではありません。 そこで本記事では、アプトポッドに少しでも興味を持ってくださった皆様に、 「なるほどアプトポッドとはこんな会社なんだな」 と納得していただけるよう、 アプトポッドのこれまで、現在、そしてこれからについて 、解説をこころみたいと思います。 (あわよくば、私のように会社説明に難儀している自社社員に、説明の手助けになるような記事にできれば一石二鳥です) 少々長い記事になるかとは思いますが、最後までお付き合いいただけますと幸いです。 はじめに アプトポッドのこれまで 実は、創業から14年も経っています 最初の車両計測案件、そして受託開発へ 受託開発の積み重ねと、自社製品への布石 はじめての自社製品 主力製品の開発開始、そして現在へ アプトポッドの現在 ①車両計測、データ収集/可視化パッケージの提供 ②データ収集/伝送基盤としての IoT プラットフォームの提供 例えば、AI 開発のデータ収集基盤として 例えば、遠隔モニタリングのバックエンドとして その他、プラットフォームの導入コンサルティングも承ります アプトポッドのこれから 車両計測パッケージのさらなる洗練 API/SDK整備によるプラットフォームの独り立ち ロボティクス分野への進出 さいごに アプトポッドのこれまで 実は、創業から14年も経っています アプトポッドの創業は今から14年前、 2006年 に遡ります。 (もちろん当時私はまだ在籍しておりませんので、このセクションのお話はすべてCTOにヒアリングした内容をもとに記載しております) 創業当時は、まだ現在のような自動車計測やIoTプラットフォームといった事業はなく、 プロジェクト支援やマーケティング支援を受注する小さな会社だったそうです。 最初の車両計測案件、そして受託開発へ アプトポッドにとっての転機は、 2010年 に受注したとある案件です。 当時車体改造を手がける他社様と連携して受注した案件のなかで、試しに作製した遠隔モニタリングのデモアプリが全てのきっかけでした。 初めは弊社の技術力を知っていただくためのデモに過ぎませんでしたが、 お客様も弊社自身も、これをきっかけに「遠隔から車両状態が確認できること」の有用性に気づき、 遠隔モニタリング自体が次第に案件の重要な開発項目となってゆきました。 これが、アプトポッドにとっての初の車両計測案件です。 現在の CTO であり、弊社最初のエンジニア社員である梶田 の JOIN により受託開発を受注できるようになったこともあり、 ここから車両計測案件を中心とした受託開発へ事業領域を拡大していきます。 受託開発の積み重ねと、自社製品への布石 とはいえ、受託開発事業を開始してから現在まで、車両計測に関する案件ばかりをこなしてきたわけではありません。 弊社としても、はじめから車両計測が当たると確信して一極集中していたわけではなく、 車両とは全く関係のないUI開発を請け負ったり、メール配信システムの構築からアミューズメント系のアプリ開発までとにかくなんでも受注し、 お客様の要望に全力でお応えしながら、弊社としての生き残る道を模索しました。 幸い、こうした模索を続けるあいだにも、車両計測に関する案件に関しては、何度もご依頼いただくことができました。 受注した案件で、車両情報の遠隔監視やバスの車内サイネージなどをいくつも手がけていくなかで、 自動車関連データに関するノウハウや、データの可視化表現に関するノウハウを蓄積してゆきました。 こうして様々なお客様とともにいくつものサービスを作り上げ、ノウハウを蓄積していくうちに、 複数の案件において共通のワークフローや機能セットがあることに気づきます。 より早く、より高品質にお客様の要望を実現するためには、共通項をまとめたアプリケーションが必要との考えのもと、 現在の intdash (バックエンドシステム) や Visual M2M Data Visualizer (可視化ダッシュボード) の前身となる、 自社製品第一号の企画開発がスタートしました。 はじめての自社製品 それから少し経った 2015年 、当時まだ IoT (Internet of Things) という言葉もそこまでバズっておらず、 M2M (Machine to Machine) という言葉が多少優勢だった時代に、 「自動車を携帯通信網でインターネットに繋げて遠隔監視」 という、当時としてはそれなりに尖った機能性を持った自社製品第一号が正式リリースを迎えます。 (その当時の名称を受け継いで、現在も弊社では Visual "M2M" という名称をブランド名として使用しています) trends.embed.renderExploreWidget("TIMESERIES", {"comparisonItem":[{"keyword":"IoT","geo":"","time":"2010-01-26 2020-02-26"},{"keyword":"M2M","geo":"","time":"2010-01-26 2020-02-26"}],"category":0,"property":""}, {"exploreQuery":"date=2010-01-26%202020-02-26&q=IoT,M2M","guestPath":"https://trends.google.com:443/trends/embed/"}); 主力製品の開発開始、そして現在へ 自社製品第一号をリリースしたことで、大手自動車メーカー様にもご採用いただくなど、自社製品を活用した車両計測案件の受注が本格化しました。 ツールとしてハードに使い倒されていくにつれ、性能や機能、セキュリティなど他方面において強化すべきポイントが見つかるようになります。 これらを改善すべく、 2017年 より企画開発がスタートしたのが intdash と Visual M2M Data Visualizer であり、現在も利用されている弊社の主力製品です。 Visual M2M Data Visualizer(旧名称 Visual M2M)PV映像 intdash の全体像 このように会社の成り立ちをいまいちど振り返ってみると、着実に現場で経験を重ね、その経験をもとに自社製品を育んできた歴史が見えてきます。 (実際、新入社員の口から一番多く言われる言葉は、 「ベンチャーなのに意外と堅実な会社なんですね」 というものだったりします) また、とてもありがたいことに、弊社が現在お手伝いをさせていただいているプロジェクトにおいても、 「開発して納品して終わり」 、というだけではなく、 お客様と一体となって、継続開発をしながらサービスのあるべき姿を模索していくような関係性を構築させていただくことが多くあります。 お客様とのプロジェクトを進めていくなかで、弊社としても経験値やノウハウをためながら、お客様と二人三脚でサービスを進化させていく 、というのは、 会社の創業当時から変わらずアプトポッドの根底に流れる、大切にしていきたい価値観のひとつです。 アプトポッドの現在 こうした経緯を経て、現在のアプトポッドの主な事業領域は、以下の2つから構成されています。 ①車両計測、データ収集/可視化用パッケージの提供 ②データ収集/伝送基盤としての IoT プラットフォームの提供(と、それを利用した受託開発) それぞれの事業について、以下でもう少し詳しく説明をしていきます。 ①車両計測、データ収集/可視化パッケージの提供 この領域では、 自動車や重機、建機などの車両全般を対象とした、データ収集や遠隔監視のためのツール群 を Automotive Pro パッケージ として提供しています。パッケージの中には、車両からデータを取得するための 車載アプライアンス (intdash Terminal System) 、 クラウド上のデータ収集管理プラットフォーム (intdash REST API / Realtime API) 、 可視化用Webダッシュボード (Visual M2M Data Visualizer) などがセットになっており、車両計測で必要となるツール群をワンストップで揃えることができます。 アプトポッドのこれまで でも述べたように、 これまでお客様と積み重ねてきた車両計測案件で蓄積したノウハウを結集して企画開発しておりますので、 車両計測や遠隔監視で問題になりがちな以下のような困りごとを解決するソリューションとして、 大手自動車メーカー様を含め、自動車開発の現場などでご利用いただいております。 データ量が多すぎて詰まってしまい、リアルタイムに可視化/監視できない 電波状況の悪いところで、データが伝送できずに欠損してしまう OSSのダッシュボードツールではリアルタイム性に満足がいかない データが多すぎてOSSのダッシュボードツールではうまく可視化できない ②データ収集/伝送基盤としての IoT プラットフォームの提供 こちらの領域では、もともと車両計測パッケージのバックエンドシステムであった intdash を、 車両計測以外のユースケースでもご活用いただけるよう、 IoT プラットフォーム として提供しています。 例えば、AI 開発のデータ収集基盤として 最近の AI 開発 においては、まず AI モデルの作成のために学習用データの収集が必要となりますが、 データの収集に課題を抱えていらっしゃるお客様にお声がけいただくことが増えてきています。 intdash は、 車両計測で培った高頻度データの伝送や蓄積、管理のノウハウ を生かし、 大量のデータをより早く、より確実に伝送、収集することに長けたプラットフォームとなっておりますので、 このようなユースケースを求められるお客様にも、データの収集基盤としてご好評いただいております。 例えば、遠隔モニタリングのバックエンドとして また、もともと車両情報の遠隔監視から始まった事業でもありますので、 遠隔地に存在する自動車やロボットなどの移動体の リアルタイムモニタリングやフリート管理 のための バックエンドプラットフォームとしてもご活用いただいております。最近では、 低遅延、大容量、双方向性などの機能性を評価いただき 、移動体のみならず、工作機械のモニタリングなどのFA分野でも、だんだんとお声がけをいただく機会ができるようになりました。 ご参考 (最近の事例については掲載が追いついていないものも多く、ご不便をおかけいたしております) 燃料電池小型トラック開発プロジェクト マーシャル諸島共和国ソーラーEV共同実証実験プロジェクト その他、プラットフォームの導入コンサルティングも承ります 様々なお客様のバックエンドシステムとして intdash が活用され始めたとはいえ、 これまで自社製品のバックエンドシステムであったという製品の経緯や、 (ベンチャー企業らしく) 常に機能が追加され、進化を続けているプラットフォームであるという性質上、 2020年現在でも、実はまだプラットフォームがもつ全てのAPIやSDKを開示できているわけではありません。 (みなさまにプラットフォームの全ての機能をご利用いただけるよう鋭意対応中です。本年中には正式にAPIとSDKの正式リリースを予定しておりますので、今しばらくお待ちください) それでは、APIやSDKが揃っていない中で、お客様にどのようにソリューションをご提供しているかといえば、現在は、 製品を熟知した弊社のソリューションアーキテクトがそれぞれのお客様のご要望をお伺いし、最適なアーキテクチャをご提案させていただく 、という形態で、お客様のプロジェクトをお手伝いさせていただいております。また、ご提案したアーキテクチャは自社で開発を請け負い、責任を持ってデプロイ、運用までお付き合いさせていただいております。 アプトポッドのこれから 最後のセクションでは、現在の事業領域を踏まえて、今後の展望を描きたいと思います。 車両計測パッケージのさらなる洗練 現在のアプトポッドでは、過去の経緯から車両計測や自動車関連のPoCプロジェクトが数多くあります。 すでに国内大手自動車メーカー様や建機メーカー様において導入実績を積んできており、 車両計測においてはある程度の認知度を獲得してきていると自負しております。 もちろんこの方向性は、 導入していただいているお客様からの意見を伺いながら、より充実したものへと拡充させてゆきます。 最近では、車両情報と合わせて動画データを取得したいというご要望も多く、ハードウェア部門では自社企画のビデオエンコーダデバイスを鋭意開発中です。 こちらの詳細につきましては、ハードウェア部門より公開されている以下の詳細記事をご参照ください。 tech.aptpod.co.jp さらにこれからは、お客様のワークフローに合わせてより柔軟なシステム構成が取れるよう、 自社企画製品の開発だけでなく、 サードパーティ製品との連携も強化していく方向性も模索していきたいと考えております。 API/SDK整備によるプラットフォームの独り立ち アプトポッドの現在 でも述べましたが、 現在の intdash は、まだ弊社アーキテクトによる導入サポートが必要な段階にあります。 今後、より多くのお客様に弊社のプラットフォームをご利用いただくためには、 弊社からアーキテクチャをご提案させていただくだけでなく、お客様ご自身でアーキテクチャを考え構築できる必要があると考えております。 そのために、APIの開示やSDKの提供、サンプルソース、ドキュメントの拡充を推進していく予定です。 また、他社様とのパートナーシップやOEM展開も視野に入れ、より多くのお客様へソリューションを届けられる体制を確立してまいります。 ロボティクス分野への進出 弊社のお客様のうち、最近自動車開発の延長としてロボット開発に興味を持たれている方々が増えてきているようです。 弊社がお手伝いさせていただいている案件のなかにも、ロボット自体の開発や、自動車開発へロボティクスの知見を活用するPoCが、 見受けられるようになってきました。 自動車とロボットは、 高頻度なデータを発するインテリジェントな移動体 という点において それほど遠くない領域、違いに知見を転用しあえる領域であると言えます。 実際、最近では ROS をベースとした Autoware が自動運転分野で活用されるなど、自動車とロボットの境界が曖昧になりつつあります。 弊社としては、 現在の主戦場である車両計測、自動車分野のみに留まることなく、車両計測で培った知見をロボティクス分野にも応用していきたい と考えており、 現在ロボティクス分野への進出準備を進めています。 最近の展示会において、だんだんとその取り組みは社外公開をし始めており、 今後は実際のお客様事例への適用を目指して、ますます対応を加速させていきます。 tech.aptpod.co.jp さいごに 駆け足にはなりましたが、アプトポッドという会社の成り立ちや、現在の事業領域、今後の展望までを解説してまいりました。 振り返りもかねて、ざっくりと内容を要約しておきます。 車両計測、データ収集用のパッケージ を提供している会社です 自動車やロボットのような 高頻度なデータの収集/伝送/可視化 に強みがあります データ伝送プラットフォームは、 車両計測以外のユースケースでも ご利用いただけます 自社IoTプラットフォームをベースとした 受託開発 も承ります 今後は、API公開を推進し、 PaaSやミドルウェアとして 、ご利用いただける形態を目指します 今後は、自動車分野にとどまらず、 ロボティクス分野 にも挑戦してまいります 最後の最後に少しエモいお話を少々させていただき、締めくくりといたします。 ここ数年で開発組織に関わるメンバーが一気に倍増 し、マネジメント体制が組織拡大に追いつかなくなりつつあるなかで、 CTO室体制が出来上がりやっと半年がすぎた現在、だんだんと組織規模にマネジメントが追いついてきた感があります。 一方で、会社全体の規模をみるとそこそこの大きさに見えるかもしれませんが、冒頭でもお伝えした通り弊社は対象としているレイヤが異様に広く、 ハード、組み込み、サーバー、Web、iOS、インフラ、デザインなど、それぞれのチームに割り振ってしまえば、各チームはまだまだ小さなベンチャー時代とそう変わらない規模のままだったりもします。 何を言いたいのかといえば、 これからさらに事業領域を拡大し、より多くのお客様とともに新しいサービスを作り上げていくには、 現在の規模でもまだまだメンバーが足りていません。 もしこの記事をお読みになって、アプトポッドに興味を持ってくださった方がおられましたら、 ぜひ弊社の人事までお声がけください! もちろん、 車両計測、データ収集やIoTプラットフォームの構築でお困りになられているお客様 も、 是非弊社に一度ご相談ください。 弊社のソリューションアーキテクトが、最適なアーキテクチャをご提案させていただきます! 最後までお読みいただき、誠にありがとうございました。本記事をお読みくださった皆様と、 お客様として、仕事仲間として、お会いできることを心よりお待ちしております!
アバター
みなさまこんにちは。先進技術調査グループのキシダです。techブログは3度目の登場です。 今回は小ネタを投下してみたいと思います。 ( 前回の記事 にも登場していますが)2020年のAutomotive Worldにてご紹介した通り、intdashがついに「 音声データ 」にも対応しました! 🎉 そして 音声 といえば、皆様ご存知でしょうか・・・? そうです、AWSの「音声の文字起こし機能」で有名な Amazon Transcribe が、昨年末日本語対応したのです! 🎉 これは組み合わせてみるしかない・・・! ということで、今回は以下のようなシナリオを目指し、文字起こし機能をintdashに組み込んでみました。 iPhoneにむかっておしゃべりしてみる サーバー側で音声データをテキストに変換する 可視化ツールで音声が再生されるのと同時に文字に起こされていく様子を眺める 音声データとテキストデータ双方をintdashに通すことでデータの同期性が担保されるため、音声が再生されるのと同時にテキストに変換されていく様子を確認することができます。こうすることでAmazon Transcribeが音声のどの言葉を拾って変換してくれているのかが一目瞭然でわかる、というところがこのシナリオのGoodポイントです。 使用するサービスとワークフロー 今回のシナリオ自体は至って単純ですが、実は裏側で複数のサービスを使っています。 そのあたりを詳細に説明いたします。 Visual M2M Motion for iOS (以下 Motionとします) iOS向けに開発された、スマートフォンのマイク・カメラ・センサー情報などからデータを収集してくれるアプリになります。今回はiPhoneから音声データを取得し、サーバーに送る箇所で使用します。 Python SDK for intdash Analytics Services  (以下 Python SDKとします) 収集したデータをintdash上で加工・解析するプロセスをはさみ、intdash上でやりとりされるデータの処理を実現するライブラリです。今回は、Amazon Transcribeとの連携で使用します。 Amazon Transcribe 開発者が音声をテキストに変換する機能をアプリケーションに簡単に追加できるようにする、自動音声認識 (ASR、automatic speech recognition) サービスです。 Visual M2M Data Visualizer (以下data-vizとします) intdash上でやりとりされるデータを可視化するアプリケーションです。 今回は解析結果を確認する際に使用します。 弊社製品の詳細は、以下をご参照ください。 https://www.aptpod.co.jp/products/ 上記を踏まえ、全体のワークフローは以下のようなイメージになります。 ワークフローイメージ iPhoneのMotionアプリから音声データをintdashにアップロード 音声ファイルをPython SDK経由でS3にアップロード Amazon Transcribeを用いてテキスト変換 変換後のテキストデータを時系列データに変換し、intdashにアップロード .data-vizで音声とテキストデータを表示 というイメージです。せっかくなので、準備手順も詳細にご紹介したいと思います。 ※ 今回のソースコードは、Python SDKが一般公開されていない点と、今回のコードはそこまで需要がないだろうという独断から、要点のみ公開することにします。全部ほしい!という方がいらっしゃましたらぜひぜひ以下のお問い合わせ窓口へお願いいたします! https://www.aptpod.co.jp/contact/ 1.iPhoneのmotionアプリから音声データをintdashにアップロード iPhoneに向かって以下を朗読します。 ポンコツ滑舌マンにとっては大変な苦行ですが、ここがこらえどころです。 (この記事のショボさをごまかすべく、最初はみんな大好きサンドウィッチマンのコント「ハンバーガー屋」をチョイスしたのですが、著作権侵害が怖くてやめました) intdashは、100ミリ秒∼1ミリ秒間隔程度の高頻度で発生する時系列データを品質保証のないネットワークを経由して、 高速・大容量かつ安定的にストリーミングするための双方向データ伝送プラットフォームです。 intdashは、プラットフォームを構成する製品・サービスの総称を兼ねており、 INTeractive DAta Streaming Hub の頭文字を並べた略称です。 こちらから引用: https://www.aptpod.co.jp/products/intdash/ 2.音声ファイルをPython SDK経由でS3にアップロード 大きな壁を乗り越えた後は、Python SDKを使用してiPhoneからアップロードされた音声データを取得します。 ※ Python SDK自体はまだ一般公開されていませんが、一部コードを貼り付けておきます。 ### Python SDKを使用して時系列データを取得 it = lc.units().list( start=XXX # iPhoneで計測を開始した時間 end=XXX,  # iPhoneで計測を終了した時間 meas_uuid=m.uuid, # 1つ計測に紐づくuuid ) ### 取得した時系列データをファイル化 file = io.BytesIO() for us in it : pcm_list = [u for u in us if u.data.data_type.value == lowlevel.core.DataType.pcm.value] for u in pcm_list: file.write(u.data.data) with wave.Wave_write("20191220_1_test.wav") as w: w.setnchannels(pcm_list[0].data.channels) w.setsampwidth(pcm_list[0].data.bitpersample // 8) w.setframerate(pcm_list[0].data.samplerate) w.writeframes(file.getvalue()) w.close() 3.Amazon Transcribeを用いてテキスト変換 ファイル化したデータをS3にアップロードします。 S3のファイルアップロード画面 アップロードしたファイルを参照し、Amazon Transcribeのジョブを作成します。 session = boto3.Session(profile_name='****', region_name='ap-northeast-1') transcribe = session.client('transcribe') job_name = "audio_test_20200219_2" job_uri = "https://s3-ap-northeast-1.amazonaws.com/transcribe-20191219/audio/20200219_2.wav" transcribe.start_transcription_job( TranscriptionJobName=job_name, Media={'MediaFileUri': job_uri}, MediaFormat='wav', LanguageCode='ja-JP' ) 上記を実行すると以下のようなファイルがjson形式で出力され、S3に保存されます。 { "jobName": "audio_test_20200219_2", "accountId": "263480619405", "results": { "transcripts": [ { "transcript": "インド ダッシュ は 百 ミリ 秒 から 数 ミリ 秒間 程度 の (以下略)" } ], "items": [ { "start_time": "1.44", "end_time": "1.79", "alternatives": [{ "confidence": "0.2469", "content": "インド" }], "type": "pronunciation" }, ======= 省略 ======== ] }, "status": "COMPLETED" } ファイルに対して翻訳した全文と、各単語ごとの変換結果が出力されています。 items の内容を参照すると、変換されたテキストが全体のファイルのうち、どのタイミングで発言されたかが把握できるようになっています。この情報を元に、テキストデータを時系列データに変換していきます。 4.変換後のデータを時系列データに変換し、intdashにアップロード 上記の各itemごとの start_time と end_time を用いて時系列順にテキストデータを並び替え、intdashにアップロードしてみます。 for text in text_list: content = '' for con in text['alternatives']: content += con['content'] if 'start_time' not in text or 'end_time' not in text : text['start_time'] = before_start_time text['end_time'] = before_end_time ## 特定の時間があいたら文章をリセットする if before_end_time != None and float(text['start_time']) - before_end_time > 1: text_joined = '' text_joined = text_joined + content before_end_time = float(text['end_time']) before_start_time = float(text['start_time']) ## 時系列のデータ形式でアップロードする(Python SDKを使用) units.append( lowlevel.core.Unit( elapsed_time = pd.Timedelta(str(text['end_time']) + 's'), channel = 1, data = protocol.data.String(label='text', value=text_joined) ) ) 5.data-vizでテキスト表示してみる 変換したテキストデータをintdash上にアップロードすることに成功したので、もう一度朗読内容を振り返りつつ、data-vizで確認してみます。 intdashは、100ミリ秒∼1ミリ秒間隔程度の高頻度で発生する時系列データを品質保証のないネットワークを経由して、 高速・大容量かつ安定的にストリーミングするための双方向データ伝送プラットフォームです。 intdashは、プラットフォームを構成する製品・サービスの総称を兼ねており、 INTeractive DAta Streaming Hub の頭文字を並べた略称です。 さすがAmazon Transcribe、いい感じに音を拾って変換出来ている様子が伺えますね 👌 今回は音声以外にも、音声データを分析する際に用いられる音の波形情報と、スペクトログラムも同時に表示しています。1つのデータを数種類の可視データに加工し、1つの時系列で同時に確認できる点は、弊社製品の大きなアピールポイントとなります。 まとめ 今回は音声機能をテーマに、intdashとAmazon Transcribeで簡単に同時文字起こし機能が作れました。内容的には非常にあっさりしていましたが、より文字起こしの変換機能の強化を目指し、案件での使用をめざす予定です。今後も引き続き 音声データ をテーマに解析手法を検討してまいります!
アバター
ソリューションアーキテクトの榮枝です。 今回は1/15(水)〜17(金)に開催された オートモーティブワールド の展示のお話を。 自動車関連の見本市であるオートモーティブワールドにコネクテッド・カーに大きな関わりを持つ製品・サービスを提供する弊社も出展させていただきました。 (今回の展示の柱の一つ、ビデオエンコーダについては おおひらさんが記事にしています 。) 弊社としてはこのイベントに昨年も出展しているのですが、入社時期的に私にとっては初めてのイベントでした。今回は準備全体の取りまとめ役として、好奇心からその役を受けさせてもらいました。 老舗企業ならこういう出展のやり方や担当者/部署は決まっていたりするのかもですが、 私自身はこの手の取りまとめは初めてで、何をすれば良いんだろう?というところからでした。 案の定色々苦労したり得られたものも色々あったので、その話を書かせてもらいます。 当初想定した進め方 当初展示までの流れを以下のイメージに沿って進めてみることにしました。 現在の状況整理 複数の展示アイテムとそのテーマの整理 各展示アイテムの担当者決めと進捗状況の管理 (必要に応じて)展示用のデータ取得 展示アイテムから展示会場の設置/配置を決め 設営 展示当日へ 「1. 状況整理」~「3. 進捗管理」 この1~3は順々に完了してスムーズに話が進みました!。。。とは行かず、行ったり来たりしながらになりました。 なかなか思ったように話は進まないデス。 私が取りまとめ役を引き受ける前からある程度話は進んでいたようで、 その関係者に現状の聞き取りからはじめました。 .... が、 ○○という展示をしたいという話があったけれど、その実現可能性はまだ良くわかってなくて実現可能性を確認するには相応の工数が必要。その上で展示に向けてリソースを割いてもらうべきかをどう判断(調整)すべきか悩む △△という展示をしたい話があって、実現可能なのはわかっているが、(他の展示項目と並べたときに)その展示のコンセプトが自分として腑に落ちて無くて、何のためにそれを展示したいという話が上がって来たのかが分からない 以前まで使っていた展示アイテムをやめて、新しいものをやりたいらしいけどその理由が自分自身どうにも腑に落ちないから、開発陣に新しいものを用意してもらうにしてもうまく説明ができない。 といった感じでハッキリ分からないことが多く、具体的に「これを展示する」と決めるまでにズルズルと時間が過ぎていき、焦り(汗)、、、 要はコミュニケーションの問題なんですが、私自身社歴も浅く、ハッキリ理解するために相談する先のキーパーソンを見つけ出すのに時間がかかったというところが焦りポイントでした。 そんな状況だったのですが、昨年のオートモーティブワールド展示担当がうまく打ち合わせの場を用意してくれたことでなんとか話がまとまり、「何をどういうテーマのもと展示するか」が決まりました。 感謝🙏 反省の一つとして、 「忙しいキーパーソンの捕まえ方」といったテクニック/創意工夫が私に不足していたように思います。 「カレンダーが埋まっていても/slackへの反応が悪くても、捕まえる方法は色々ある。」 「3.データ取り」 何を展示するかが決まり、展示用のデータ取りをはじめました。 展示内容に向けて実験するためのデータや、 展示会場で見栄えのするデータや動画を撮ったりといった感じです。 社内の風景や社用車で街中を走ってデータ取りをできますが、 展示会場でインパクトある、もといサービスを 正しく&わかりやすく 理解してもらえるデモにするために、 なるべく良い環境でデータ取りしました。 フェイスマスク 動画に映った顔にマスキングする技術の展示です。それ自体はAWSのサービスを利用しているのですが、弊社システムに組み込んで使える一例として見せています。 これは、こちらを向いた顔が沢山映っている絵が欲しくて渋谷のスクランブル交差点を選んで撮影しました。 スクランブル交差点にて 年末の慌ただしい中、時間を見つけて撮影しに行きました。 思っていた以上にいい画がとれて、フェイスマスクを掛けてもらった結果も良くてホっ。 歩きながら撮ってみた画もあるのですが、手ブレがひどくてこちらはボツに。 外国からの観光客も同じように撮影していたりしたのですが、みんな手ブレ防止用のジンバルやスタビライザーを使っていて、、 動画撮影って奥が深いですね。 ※実際には動画で展示させていただきました。 ※フェイスマスクをかけてない元動画は公開していません。 ビデオエンコーダ用データのサーキットでの撮影 こちらは、 弊社のビデオエンコーダーの開発を担当したおおひらさんの記事 のほうが詳しいですね。 トヨタの86を借りて千葉県の茂原サーキットという場所で実車走行してデータ取りをしました。 余談ですが、86って豆腐店のイメージしかなかったのですが、最新のモデルはあれとはだいぶ違ってまたいいクルマなんですね。 自動車業界と馴染みの深い弊社なんですが、私自身は車より自転車派であんまり車に詳しくないという。。。 年始の時間が無い中サーキットの天気が悪くて一日延期したりと慌ただしかったですが、 おおひらさんをはじめ開発のハードウェアチームが取り仕切ってくれたので、 自分としては安心して見ていました。 オーディオデータ取得 弊社のシステム、今までは音に関する計測・可視化システムがなかったのですが、開発陣が頑張って年末に掛けて色々と揃ってきたこともあり、その展示も用意しました。 自動車関連の音 ということでドライバーの発声を録っても良かったのですが、エンジン音を録ってみようという視点で、ハードウェアチームが選定してくれた このギターとかの振動を拾うためのマイク をエンジン付近に固定して録ってみました。 ※こちらの動画も茂原サーキット上で撮影したものです。 (IT系(IoT系?)の弊社ですが、自動車関連の計測話が多いのでこの件に限らずこういった サーキットや変わった場所でのお仕事がちょくちょくあるのが面白い です。) 「4.展示会場の設置/配置」 展示には展示アイテムを用意するだけでなく、 会場のレイアウトを決めたり設営業者さんを手配したりといったことも必要なんですが、 当然ながらこの具体的なところを自力で行うのは難しいのでここは業者さんにおまかせです。 そうは言っても色々やり取りする必要があるのですが、 この辺りは社長や弊社のデザイナー、昨年の担当に進めてもらい、自分はなるほどこういう感じに進めるのか〜と思いながら見ていた感じです。 実際にaptpodのデザイナーさんが設計した展示レイアウト 「5. 設営」 会場設営は業者さんがしてくれるわけですが、 実際展示するデータはもちろんこちらで用意しないとなりません。 展示データ自体は上に書いたように用意できましたが、 いざ説明用に展示するとなると、さらなる準備が必要でした。。。 データを見やすく切り取ったり、 インターネット環境が良くなかったときのため(※)に動画にしておいたり、 説明の際に手間取らないように展示用PCのアカウント整理したり、 古くなってたOSを更新したり、 久しぶりに動かしたデモ用PCが壊れて動かなくなってたり...... (※Web経由で計測データを確認出来る可視化ツールがあるのですが、たくさんの人が訪れる展示開場の通信環境は悪い可能性があるため) 細かな作業が意外と多く地味に時間がかかり、ギリギリまで作業していたのですが なんとか展示会前日の1月14日(火)に間に合いました。 業者さんによるブース設営の風景を初めて見ました。 弊社だけでなく、出展社全社が同じように設営されてるので、会場はおおわらわ。 設営風景 「6.展示当日」 ようやく展示当日。 無事スタートできようやく一息、 とはいえここからが本番! 1月15日(水)〜17日(金)の3日間、私も説明員として立って、立ち寄ってくださった方々へ弊社の製品・サービスの説明や プレゼンブースでちょっとしたプレゼン形式での説明などを行いました。 プレゼンは、人が行き交う中急に始めるスタイルでした。いきなり(マイクで)大きな声で話だすのは勇気が入りますね。。 喋り出しはめっちゃ緊張しましたが、初めてしまうとなんとかなるし、なんなら少し楽しかったぐらいです。 説明員としての説明も最初は単純に自社紹介説明を繰り返していて、 興味は持ってもらえて名刺をいただくことまではできるのですが、 どうにも先につながる感じがせず、、、 ですが、以前弊社営業の方にその辺りのことのアドバイスもらったことを思い出し、 自社の話をするよりもお客さんのお困りごとを聞き出すように気をつけていくと 色々と話が弾み先の話が見えてきた、   かな? 総括 そんなこんなで無事展示会を終え、多くのお仕事のお話にも繋がりました。 大変でしたがとてもやりがいのあるイベントでした。 以下に諸々振り返りと反省点を記載しておきます。 - 1〜10まで自分でやらなきゃ!と思い込んで無駄な心労になっていた。状況共有とある程度の方向指針さえすれば、みんなどんどん動いてくれる。早い段階でもっと信頼して協力してもらえばよかった。 - 今回はっきり役割が決まってないイレギュラーな業務を経験したからこそ、役割分担の重要性とそのやり方を意識するようになった。普段の業務でも活かせるようにしたい。 - 普段関わりの少ない人とも、こういった機会があると色々話せるのでいいですね。 - あとになってからこの記事を書くことになったので、写真をあんまり撮ってなかった。記事用に色々写真撮っておけばよかったです。「うらがわ」というタイトルにしたのに、展示ブースの裏の写真を撮っていなくって後悔。 諸々反省もあるのですが、次またあれば反省を活かしてチャレンジ、、するかも。 (なかなかに忙しく、他の業務への影響も無くはなかった(汗)ので、状況次第で!) 内容としては以上ですが、ぜひ展示の詳細などに興味が出た方は弊社までご連絡ください!お待ちしています。
アバター
はじめに ハードウェアGpのおおひらです。 弊社は1/15(水)〜17(金)に開催された オートモーティブ・ワールド に出展させて頂きました。 お忙しいなか足をとめてブースに立寄って頂いた皆様、大変ありがとうございました。 car.watch.impress.co.jp www.nikkan.co.jp 本記事ではブースにて初お披露目となったビデオエンコーダ試作品の開発経緯を紹介させて頂きます。 はじめに 開発の背景 課題1:イメージセンサ撮像〜打刻のタイミング管理 課題2:複数のカメラを使用してデータ分析する際の不正確さ 課題解決にむけて フェーズ1 : 仕様検討 & 原理試作 (3ヶ月) フェーズ2 : 設計試作 (2019/6月〜12月) 基板/筐体設計 ヘテロコアのSystem on a chip (SoC)のソフト設計 FPGA設計 事前走行テスト おわりに 開発の背景 従来、弊社では移動体の動画計測が求められるユースケースに対して 民生品のWebカメラ を利用していました。特にビデオエンコーダ内蔵のWebカメラであればサーバーに送信する際の処理リソースも末端の機器側に分散できますし、そもそも民生品なので圧倒的に低コストで調達性もよいというメリットがあります。 一方、動画に加えて、車両の制御バスであるCAN(Controller Area Network)や運転者の挙動を解析するためのアナログセンサなどを含め、移動体のシステム全体のセンサデータを統合して計測するという観点では、その時系列の正確性に課題が生じます。 課題1:イメージセンサ撮像〜打刻のタイミング管理 一般に民生品のWebカメラは自動の露出モードを有する製品が多く、暗いシーンでは露出時間を伸ばしてフレームレートを落とす処理がされます。弊社が提供する産業IoTのプラットフォームである intdash では末端の機器群におけるターミナルとして車載コンピュータ上のソフトウェアが複数センサの打刻を一元的に管理しますが、カメラから出力される映像ストリームに対してはそれを受信したタイミングで打刻しますので、受信までの間に生じる遅延を管理することはできません。もちろんカメラの撮影設定パラメータを試行錯誤したり、実験的に種々の撮影環境における遅延時間を求めることは可能ですが、カメラ内のエンコード処理に起因する遅延時間も含めると完全に保証をすることは難しいと言えます。 この結果として、車両から計測したCANデータを可視化した結果と、同タイミングで計測した動画との間で数百ミリ秒の時間のずれが生じ、計測結果を統合的に見た際に違和感をおぼえるという問題が生じます。 課題2:複数のカメラを使用してデータ分析する際の不正確さ 一般にカメラのデバイスの源振には特性のばらつきがありますので、複数のカメラ間におけるフレームレートは完全に一致しません。仮に設定上は各自のカメラが30 fpsになっていたとしても、カメラ#1は29.7 fps, カメラ#2は30.4 fps …というように誤差が生じ、その結果、例えば1時間の計測を行ったとしてカメラ#1は106,920フレーム、カメラ#2は109,440フレーム…という具合にフレームの総数が異なる結果となります。 動画は結局のところ複数の静止画の集合体ですので、複数カメラの映像を統合的にデータ解析をしようとすると、まずこのフレームのばらつきの前処理に手間がとられます。 また、ひとつの事象とそれに紐づくデータを解析する際の正確性も課題となります。例えば道路上に飛び出してきた歩行者を前方カメラが捉えた瞬間、運転者の視線がどこに向いていたか、その後どのタイミングでどれぐらいまでブレーキを踏み込んだか、ハンドルをどう切ったか…など、特定のシーンに着目したデータ解析をする場合、30 fpsの動画の1フレーム周期は約33ミリ秒で他のセンサデータの周期に対して大きく、たった1フレームずれるだけで事象分析の正確さが失われるケースが生じることも想定されます。このように、 データの利用価値を高める という点でも解決が必要な課題が多くあります。 課題解決にむけて 下記の方針を立てました。 撮像のシャッタータイミングを管理できるよう、トリガ入力をもつ産業用カメラモジュールを利用する カメラモジュールの価格はピンキリなので極力低コストで導入できるものを選定する 複数のカメラの撮像タイミングを管理するためにリアルタイム性に優れた制御デバイスを用意する ビデオエンコード部の遅延時間を管理するため、リアルタイムエンコード処理も可能なデバイスを用意する 撮像タイミング信号を使ってスナップショットした時刻情報を、ビデオエンコード時にメタデータとして埋め込む 時刻情報は既存の弊社内製CANインターフェース機器と同様、源振となるクロックを機器間で共有して生成する フェーズ1 : 仕様検討 & 原理試作 (3ヶ月) このフェーズでは技術的な解決方針が機能することの見極めとして、カメラモジュールの機器選定やH.264のビデオエンコードが可能なデバイスの選定を行い、実際に評価基板を組合せて原理に問題がないか確認を行いました。技術的な実証・試行錯誤をチームメンバー2名に担当してもらい、並行して私は製品に対する要求のヒアリング・分析・対策立案→技術選定という仕様検討のサイクルをまわすことに注力しました。要求の一例をご紹介すると… カメラは複数とりつけたい、最低4つほしい バスやトラックなど商用車にとりつけたいケースもあるのでケーブル長は長くしたい、5mは必要 車両のペダルの撮影など、狭い場所にカメラを潜り込ませることもあるので筐体は極力小さくしたい カメラのレンズは広角〜望遠など、お客様のユースケースに合わせて選択肢を設けたい LTEの上り回線の限られた帯域で複数のカメラ映像を伝送したい 映像はサーバーに送信すると同時に、高画質でローカルストレージにも保存しておきたい (圧縮なしのRAWデータを解析用に保持したい) などなど。 こだわるところと割り切るところを見極めながら落とし所を探りました(こういう地道な製品開発ループまわしは大好物😇) ある程度の実現可能性が見えた段階で社内で開発計画を共有し、商品のコンセプトや想定顧客といったマーケティング観点と、コスト(試作/量産時の材料費・開発委託費)やスケジュールといった実際の設計観点で承認を頂きました。時期的には2019年の5月末頃で、この時点で 2020年1月のオートモーティブワールドでの試作機の動展示がひとつの大きなマイルストーンである ということをマネジメントと開発チームメンバーとの間で認識合わせしました。 フェーズ2 : 設計試作 (2019/6月〜12月) このフェーズでは実際に試作基板をつくり、デバイス選定や仕様に問題が無いか機能の検証を行います。特に電源部品や主要な半導体部品が確定できること、ソフト含めたアーキテクチャに問題がないことが重要です。開発メンバーの内訳は、電気兼メカ(私)とソフト・ロジック(FPGA)設計それぞれ1名ずつの3名体制。 基板/筐体設計 回路設計を弊社で行い、基板のアートワーク〜製造と筐体の選定(熱設計含)・部品調達を協力会社様にお願いいたしました。今回は設計スケジュールの短縮のために、主要な半導体に周辺部品が既に実装されているSystem on Module(SoM)を利用する方針にし、回路設計に約1.5ヶ月、AW〜製造で約1.5ヶ月で、試作品の納品までトータル約3ヶ月の設計期間となりました。 基板が納品されてからは基本的な電源性能の確認やペリフェラルの電気的な検証を行い、必要に応じて一部回路や部品の改修を実施してソフト・FPGAとの結合を進めました。 ヘテロコアのSystem on a chip (SoC)のソフト設計 評価ボードベースの検討から、試作機で使用するSoMを利用した検討に移行しました。まずはSoMのメーカーが提供しているキャリア基板を利用して映像入力や周辺ペリフェラル(USB, Ethernet, HDMI etc)の機能確認をしつつ、試作基板むけのピン設定ファイルなどを作成し、試作基板のハードウェアの検証がひと段落した段階で実際に結合してソフトを立ち上げるという手順です。 ソフトウェアの設計観点では、Linuxの映像ストリーム処理(GStreamer)でタイムスタンプの扱いを修正したり、リアルタイム処理に利用する小規模のARMコア上のReal-Time OS(RTOS)のソフト設計が必要だったり。 過去の記事 を見て頂くと分かるとおり、ひとつのチップに複数のARMコアやハードウェアアクセラレータが混在するSoCを採用しています。担当メンバーはこういったSoCを扱うのは初めてだったにも関わらず、奮闘してくれて大変ありがたく頼もしい限りでした。 FPGA設計 今回の試作品では、カメラのシャッタータイミング制御や映像の縮小・合成処理のために前段に FPGA を利用しています。こちらも評価基板ベースの検討からはじめて、主に下記の機能を実装していきました。 カメラとの映像伝送I/F (1.8Gbpsの高速シリアル通信を4ch) SoCとの映像伝送I/F (パラレル変換して後段に送信) 映像の縮小 (簡易的なピクセルの間引き) フレームメモリを利用した4画面合成 組込みCPU (XilinxのMicroBlazeでFreeRTOSを動作させ、HostとなるSoCやカメラと制御通信する) FPGAのメモリインターフェースを扱うのが初めてだったり、 高位合成で映像処理を作ってみたり と、開発メンバーにとっては初めてづくしの業務が多かったと思いますが着実に進めて頂き大変助かりました。 事前走行テスト これまで挙げてきた機能のインテグレーションが概ね完了してカメラからサーバーへの映像の疎通ができたのが2019年の12/20頃で、年末休みに入るまでの1週間で実際に車に積んで市街地走行をするという検証を進めました。なかなか痺れるスケジュール…ですがこれも計画通り🙃 カメラの露出タイミング制御や画質調整、4画面合成機能のバグ修正、サーバー側のソフトウェアとの疎通の問題などを順番に潰しつつ、新年明けてからは展示会用のデモ計測のために千葉県の 茂原ツインサーキット をお借りして実車計測を行いました。 86にからみつく計測器たち 4-camera demo 上のデモ映像は、サーキットにおける実車計測の結果を VisualM2M という可視化ダッシュボードで再生したものです。本試作機で計測した動画(計5ch)や、オーディオ(エンジン音を録音するマイク)、車両のCANデータから可視化したハンドルやスピードメータを統合してWebブラウザ上で可視化しています。 特に4画面合成された左下のハンドル俯瞰映像と、ダッシュボード上のハンドルのイラストの動きに注目して頂くと完全に同期していることがわかります。これは本記事で紹介したビデオエンコーダと弊社製のCANインターフェス機器が同一のタイムスタンプを生成して打刻しているからこそ可能な計測と言えます。 おわりに 展示会で頂いたフィードバックをもとに、一旦仕様の見直しをしつつ、確実に商品化を進めてまいります。 具体的には、 カメラの接続を取り回ししやすいケーブルに (汎用のインターフェースを採用) 全体の材料費低減 (一部SoMをやめてICを基板実装したり筐体を小型化したり) カメラモジュールの再選定 (今回は自作したものの、量産コスト観点でOEM/ODMも検討) 製造性の改善 画質(ダイナミックレンジやエンコードの画質) システム全体の遅延低減 (SoCのHWアクセラレータはまだまだ使いこなしの余地あり) 各種信頼性試験やEMC試験 などなど、まだまだやることは山積みですが、弊社メンバー皆で協力して良いモノを作っていきたいと思います。 動画と各種センサデータの時系列解析に課題をお持ちの方はぜひ弊社までご連絡下さい。よろしくお願い致します。
アバター