TECH PLAY

アプトポッド

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

252

はじめまして、今回記事を書かせていただきますSREチームの金澤と申します。よろしくお願いいたします。 自己紹介 前職はとある会社の情報システム部門に在籍して自社のサーバやネットワークの管理といった社内インフラのあれこれを担当していました。 そんな中、社内インフラの更改を担当する機会がありました。更改先として候補に挙がっていたパブリッククラウド(AWS)に触れ、パブリッククラウドが持つサーバ資源調達の迅速性やリソース変更時の柔軟性を文字通り身をもって体験し興味を持つようになりました。 更改以降も自主的に学習を続けていましたが、次第に自分のキャリアもそちらに寄せていきたいと考えるようになりました。そして今、アプトポッドとご縁がありましてSREチームとしてインフラ構築・運用業務にあたっています。 入社のきっかけ HW設計~アプリケーション開発・デザインを統合し製品提供する難易度の高さ 成長できそう 製品が持つ社会貢献性の高さ モダンな技術の採用 製品がカッコよい の箇所を魅力的に感じ入社を決めました。製品のカッコよさは重要ですよね。製品の詳細は 製品紹介 をご覧ください。 キャッチアップの必要性 前職は管理が主な担当業務だったため、開発も含む本職に携わるにあたりキャッチアップの必要性があると感じていました。 本記事では、弊社のコミュニケーション・開発環境を簡単にご紹介するとともに、私自身がどのようなキャッチアップに取り組んでいるかご紹介できればと思います。 私のようにキャリアチェンジに取り組んでおられる方や、弊社に興味をお持ちの方に対して参考となれば嬉しいです。 入社直後の課題改善 弊社の神前(こうさき)のエントリにもあります通り、現在アプトポッドではリモートワークを前提とした勤務体系で社内コミュニケーション基盤としてSlackを利用し各種業務を進めています。 tech.aptpod.co.jp Slackでは基本的な連絡などは滞りなく進む反面、質問や確認で時間を要することを課題に感じていました。入社直後はプロダクト内の言葉や略称を把握できず、どうしても内容を正確に掴み取るまでに時間を要するものと思います。私もまさにそちらに当てはまっていて、業務で取り扱うことになるインフラ構成の詳細や至った意図など、ヒアリングしたい内容が生じていました。 質問のインプットがアウトプットを上回る状態で、チャット上へ随時質問を垂れ流すことも方法として微妙と感じていたこともあり、一先ずSlackの個人スペースにメモとして適宜記録していました。ただ、ご存じの通り個人スペースは自分一人のクローズドな環境のため他者との連携が無く、課題解決に向けどのように進めようかと考えていました。 アプトポッドでは個人が社内でオープンなチャンネルを持ち、 作業進捗 疑問点 気になった(技術)情報 ネタ をポストする文化があります。チャンネルを作成することや、他の人のチャンネルに参加することも自由で、ポストした内容にスタンプなどのレスポンスがあったりもします。率直に良い文化だなと感じています。そして、私の抱いていた課題もそちらに倣ってアウトプットすることで改善が図られることを期待し最近運用を始めました。まだまだ運用期間は短いですが自分の状況のアウトプットを継続することで疑問の解決を促し、自身の成長に繋げていきたいと思います。 開発フローのキャッチアップ チーム開発フローへの順応 アプトポッドではGitlabをVCS *1 に採用しコード管理を行っています。Gitは前職でも使用の経験がありましたが(様々な事情があり)自身の作業履歴を残すことを目的とした個人運用でした。そのため、Gitを使用する上で主流となるチーム開発のフローに順応することを心掛け行動するようにしています。 具体的に言いますと以下のような内容になります。 小さな単位でのコミットを意識する 思いついた多数の修正を含めて fix 等とやらない。 分かりやすいコミットメッセージを心掛ける フォーマットに従うなど運用ルールを確認する。 チームで使用していることを意識する。 ターミナルへツール導入し操作性の改善を行う。 何れも基本的なところですが、VCSの運用方法は会社やその会社が持つ文化に左右され一概には決まらない印象が強いです。このような基本的な内容を確認することで運用方法はもちろんのこと、会社文化の一端を感じ所属している場所に慣れる方法として有効であると考えています。 サンドボックスを利用したIaC *2 のキャッチアップ アプトポッドではお客様に提供するインフラを Terraform および Ansible のコードベースで管理しています。 業務を進める中でインフラリソースを自由に作成・変更・削除することができる環境が欲しくなることがあります。提供されているドキュメントと実動作の乖離確認やパフォーマンスの測定、また既存環境には影響を及ぼさず安心して試行錯誤したいというモチベーションから生まれるものです。 アプトポッドでは、開発者や希望者にパブリッククラウド(AWS)の個人アカウントを払い出しており各々が使用することができます。そのため個人アカウントをサンドボックスとして利用して前述のような検証を自由に行うことができます。これは「社員各々の成長、開発スピードの向上のため」という目的の元で進められていて皆幸せとなれる良い文化だなあと感じています。 *3 私自身もこれには非常に助けられており、自身の理解促進に役立っていると実感しています。特にコストがかかる機能検証はプライベートアカウントで行うのも抵抗がありますよね。 守備範囲を広げる 構築作業で使用するツールがPythonの2系での実装であったため、構築作業と並行して3系への移植を行いました。そして、このような作業に関わらず各種作業を通じて気づいた点は、社内Wikiに反映することを都度行っていました。既存環境の改善の一環なのですが、さらにもう一点、自身の守備範囲を広げるというねらいがありました。 まだ、構成を十分に把握していない不透明な環境の中で少しでもバリューを発揮できるよう得意な所を増やし、自身の活動の基礎を作るよう意識していました。この考えは参考にさせていただいた ブログエントリ があります。エントリ中に表現されている「庭」という考え方にはすごく共感しており、引き続き実践していきたいと考えています。 知識のキャッチアップ 主に入社前の取り組みになりますが今まで得た知識の振り返りや新たなインプットのために以下の書籍を読みました。 いちばんやさしいGit&GitHubの教本 みんなのコンピュータサイエンス マスタリングTCP/IP入門編(第5版) 過去に購入した書籍を復習のために読み返しました。この中でも有名なマスタリングTCP/IP入門編は一通りの内容をおさらいしたかったため今回は浅く内容をさらったのですが、読んでみて改めて体系的に内容が網羅されている本だと思いました。昨年 第6版 が発売されたようですね。 [試して理解]Linuxのしくみ~実験と図解で学ぶOSとハードウェアの基礎知識 Software Design LinuxのしくみはLinuxの基礎動作を図やプログラムで追えることができるため選択しました。Software Designは毎号特集しているテーマについて初学者が触れやすい構成をしていることが多く、テーマにざっくりと触れたいと思う時には購入しています。 入門監視 SREサイトリライアビリティエンジニアリング 有名なSRE本ですが、まずは一通り読み終えたく少しずつ進めています。 最後に 本記事ではアプトポッドへ入社し業務に携わるにあたり取り組む必要があると感じていたキャッチアップについて、弊社のインフラを交えお話させていただきました。挙げた内容は今後も継続していき、アプトポッドや弊社プロダクトへ貢献できるようこれからも努力したいと思います。 *1 : Version Control System *2 : Infrastructure as Code *3 : もちろん自身でコストの確認を行いつつ使用することにはなります。コスト意識は大事。
はじめに 動画ストリーミングサービスにおいて、動画の遅延を測定したいというニーズは多いと思います。 動画が遅れる要因として以下3つが主に考えられると思います。 ネットワークの遅延 アプリケーションで行う処理による遅延 動画エンコード遅延 動画デコード遅延 これらをすべて含んだ遅延の測定は比較的簡単に計測可能ですが、要因を切り分けて測定するのは工夫が必要かと思います。 今回、弊社製品 intdash の特徴である、複数のデータソースでタイムスタンプ管理ができることと、オリジナル治具を利用して、この動画エンコード遅延の測定をしてみた話をしたいと思います。 ハードウェアチームの塩出が担当します。 はじめに 本題に入る前に、弊社製品の説明 intdashを使ったエンコード遅延測定の考え方 測定するための治具 特徴 intdashと治具を使った動画エンコード遅延測定 治具についての余談(FPGA内のことについて) FreeRTOSでの実装 おわりに 本題に入る前に、弊社製品の説明 弊社製品 intdash を使用すると、映像やCAN、アナログデータ等、異なるデータソースでもタイムスタンプを一元管理することができ、webアプリケーションの Visual M2M Data Visualizer (以下Visualizerと呼ぶ)を使って取得したデータを、そのタイムスタンプを元に再生することが出来ます。 具体的には、車が発しているCANのデータと、ドライバを撮影した映像を同期させて取ることができ、それをwebアプリケーションで確認出来ます。この仕組みを使うと例えば、車が発しているハンドル角度に相当するCANデータと、その時ドライバが操作しているハンドルの角度が一致してる状態で確認できるということになります。 もちろん、そのままではエンコードの遅延が乗ってしまうので、お客様へ提供する製品ではこのエンコード遅延分は事前に測定して補正するようにしております。 intdashを使ったエンコード遅延測定の考え方 intdash での打刻タイミングは、CANのデータはCANデータを受け取った時、動画はエンコードされたデータを受け取ったときとなっています。なので、もしCANと被写体が同じデータを発するものだとすれば、 Visualizer を使って確認すると理想的には、ある時間における動画データとCANのデータは同一のものになっているはずです。仮にこれが異なるとすると、その時間分がエンコードの遅延ということになります。図にすると以下の様になります。 測定するエンコード時間の説明図 測定するための治具 CANと被写体が同じデータを発するものというと、身近なもので言えば車のステアリングが相当するのですが、手軽にデータが取得出来ないのと、ステアリングがどれくらい回転しているかを動画で把握するのは目分量になってしまうので測定向きではありません。そこで、今回は Terasic社製のFPGA評価ボード DE10-Lite を使用して測定のための治具を作ってみました。 動画エンコード遅延測定治具 特徴 DE10-Lite は、7segディスプレイが付いた評価ボードで、CANの出力部分に関しては自作しました。簡単に特徴をまとめると以下のようになります。 7segディスプレイは1msec毎に更新 7segディスプレイと同タイミング(約20usec差)で同じ内容のCANを出力(以下の図を参照ください) 右下のLEDは1msec単位のインジケータ 7segディスプレイはダイナミック点灯ではないので、クリアに撮影可能 Visualizer で確認した時、同一時刻における映像の7segディスプレイが示している値と、CANが示している値の差がエンコード時間となる CANと7segの更新タイミング intdashと治具を使った動画エンコード遅延測定 上記治具と intdash を組み合わせて動画エンコード遅延を測定した結果を以下の図に示します。この図は撮影済みのデータを Visualizer で再生したときの様子です。 図の黄色の枠で示したものがCANのデータで、965.294秒を示しています。図の青色の枠で示したものが動画データで、965.193秒を示しています。この差分が動画エンコード遅延を示すので、約100msecの遅延だということがわかります。 なお、この遅延は事前に測定しており、お客様へ提供する際はその分補正しております。 Visualizerで確認できる遅延量の例 治具についての余談(FPGA内のことについて) 今回、ソフトコアである NiosII を使用して、7segディスプレイの更新タイミングと、CANの出力のタイミングを制御しました。 NiosII のプログラムは結局ベアメタルで実装してしまいましたが、 FreeRTOS バージョンでも実装は試しました。 今回使用したQuartusのバージョンは18.1だったのですが、このバージョンだと FreeRTOS のコンテキストスイッチがうまく働かず、そのままでは正常に動作しませんでした。なので、正常に動作するように変更し、FreeRTOSのgithubの方に プルリクエストは出した のですが、コロナの影響で現物確認ができないと言われ、ペンディングになっています。それでもよろしければ活用してフィードバッグをいただけると嬉しいです。 FreeRTOSでの実装 上記プルリクエストでFreeRTOSが使えるようになったので、FreeRTOSでも実装してみました。実装は1msecごとにqueueを出力するタスクと、そのqueueを受け取ってCANを出力するタスク、7segを更新するタスクという構成で行いました。その場合、CANと7segの時間差は200 usecと10倍くらい精度が落ちてしまいました。本来はここからチューニングしてパフォーマンスを出す作業を行うのですが、今回はFreeRTOSを入れること自体に時間がかかってしまったのと、ベアメタルでも事足りていたので、特にチューニングせずにベアメタルを採用することにしました。 今回は残念ながらFreeRTOSは不採用でしたが、FreeRTOSのコンテキストスイッチ回りの実装をちゃんと追ったので今後に活かしていきたいと思います。 おわりに 今回は intdash とオリジナル治具を使って、動画エンコード遅延測定をしてみた話を紹介しました。CANと7segを組み合わせるというあまり見ない構成な気がしますが、デジタルっぽく測定できるので個人的には気に入っています。7segの映像を画像処理して、数値をデジタル化できれば完全にデジタル化できるのでテストツールとしては完成かなと思っております。そこはこれから挑戦していきます。 余談ですが、自分的に一番伝えたかったのはNiosII上でFreeRTOSを動かすことだったりします。バグ報告はあるのですが、解決まで行っておらず苦労したので、この記事が NiosII上でFreeRTOSを動かす上で役に立てれば幸いです。
はじめに こんにちは、aptpodに入社しそろそろ1年になりますWebチームの松本です。 aptpodでは日々フロントエンドエンジニアとしてReact/TypeScriptを用いた、お客様向けアプリケーションのUIを実装しています。 Reactは実は入社してから初めて触ったフレームワークでしたが、頼れる先輩方のサポートもありつつ日々成長を感じながら開発に励んでいます。 入社当初から開発に関するノウハウやコードに関する考え方など、具体的なプログラムのプロジェクト構成やコンポーネントのファイル分け、コード全体の品質を担保できるよう様々な工夫があり、入社する前から知っていれば…と思うことも多くありましたのでそれらについて今回ご紹介しようと思います。 コンポーネントのファイル構成 まずコンポーネントとはWebページのビューを切り出した部品、つまりボタンやアイコン、またそれらを含む集合体であるヘッダーやメニューを指す言葉です。 コンポーネントと一口に言ってもコードとして記述するには色々な情報を詰め込む必要があります。 ボタンのコンポーネントであれば、ボタンの色形といったスタイル情報や、ボタンを押したときの日時の変換や文字列のフォーマット、またそれらのコードがちゃんと想定通りに動いているかチェックするためのテストコードなどになります。これらを一つのファイルにまとめて記述した場合、コードの可読性が著しく損なわれるため、コンポーネントで利用する基本的なファイル構成をテンプレート化し、それぞれのファイルに対し記述を行います。 以下の図はコンポーネントのファイル構成になります。共通で使うものや、ファイルとして大きくなってきた場合などは外出しすることもありますが、基本的にはこれを使っています。 コンポーネントのファイル構成 component.tsx React.FCを宣言しReactコンポーネントのメインとなるファイル constant.ts componentやutilsで使う定数を置くファイル index.ts importしやすくするためのフォルダ内のexportを格納するファイル style.tsx styled-components用のCSS設定を格納するファイル test.stories.tsx storybookで利用するためcomponent.tsxからインポートし表示するためのファイル test.stories.style.tsx test.stories.tsx内で利用するstyled-componentsのCSS設定を格納するファイル utils.ts component.tsx内で利用するロジック用の関数を格納するファイル utils.test.ts jestを利用しutils.tsからインポートした関数のテストコードを格納するファイル テンプレート生成ツール 上記のように各コンポーネントにテストを書くなどしてファイルが増えてくると新しいコンポーネントを作るときにファイル生成が手間になってしまいます。そのようなときにコマンドでコンポーネントのファイル郡を生成してくれるものとして次のものを使っています。 hygen 対話式コードジェネレータCLIでテンプレートに沿ったファイルの生成を行ってくれるツールになります。 npm、yarnで導入でき先に示したテンプレートを設定しコマンド入力しコンポーネント名を入れると生成してくれるようになります。 www.hygen.io ユニットテストツール コンポーネントは見た目に関するビュー部分と動きや計算を行うロジック部分に分かれますが、こちらは主にそのロジック部分をテストするツールになります。前述のコンポーネントのファイル構成ではutils.ts内で使われるライブラリとして以下のものを使用しています。 jest npm、yarnですぐに使えるテストツールです。utilから関数をインポートし入力に対する結果を記述することで、コマンド入力もしくはymlの記述でCI時に自動テストを行うことができます。 下図のように、設定した拡張子(ここでは.test.ts)のプロジェクト内のファイルのテストコードが実行され、結果が表示されます。 jestjs.io jestテスト結果 ビジュアルリグレッションテストツール こちらは先程と違いビュー部分に関するテストツールになります。ビジュアルリグレッションテストとは、画像の保存を行い、コミット後にコミット前との比較を行うことで、意図したコンポーネントだけが変更されているか、意図していないコンポーネントに変更を及ぼしていないかを検出するテストです。 前述のコンポーネントのファイル構成ではtest.stories.tsx内で使われるライブラリやツールとして以下のものを紹介します。aptpodではアトミックデザインを採用したコンポーネント構造になっており、基本的な部品コンポーネントを変更した場合、影響の出る親コンポーネントは多数に渡ることもあり、目視確認では限界があるためこれらを導入しています。 storybook こちらはテストツールというよりは作成したコンポーネントを一覧化し、開発中のコンポーネントのUIを確認するためのツールになります。アトミックデザインを採用していればフォルダ構造そのままで一覧化されるので、わざわざページアクセスし対象のコンポーネントが表示されるページまでリンクを追ってという手順を踏まずに済みます。 storybook.js.org storybook画面 storycap このツールではstorybook上で表示されるコンポーネントの画像をスクリーンショットとしてフォルダにまとめて保存してくれるツールになります。 ローカルでは保存先のフォルダを指定することでコマンド一発でスクリーンショットを生成してくれます。CIでは保存先をAWSのストレージサービスであるS3などにしておき、後の変更時の比較に使います。 github.com storycap出力図 reg-suit こちらは先程S3に保存した画像と新しくコミットされた内容の画像を比較し、変更があった場合はその差分を検出してくれる機能になります。CIにAWSのキーを環境変数にセットしregconfig.jsonに設定を書き込むことで動いてくれます。 reg-viz.github.io gitlab上でのreg-suit結果 まとめ 今回はコンポーネントのファイル構造という目線から、自動テストなどを用いてどのように品質を向上させていくのか、それらを組み合わせコンポーネントとしてどのように運用しているのか説明させていただきました。各ツールの詳しい使い方は割愛させていただきましたが、Webチームがどのように開発を進めているのかその一部でも伝わったならば幸いです。私個人としても今後ともフロントエンドを含むWebに関する知識や経験をもっと身につけ、本テックブログを通して情報発信していけたらなと思います。
研究開発グループで機械学習に関係する仕事を担当している瀬戸です。前回は、 fastaiで学習に使う関数をApache MXNetで真似してみた - aptpod Tech Blog を紹介させて頂きました。今回は、SageMaker Python SDKのMXNetで利用できるGluonCVのモデルを、SageMaker Neoでコンパイルし、Jetson tx2上でDLRを用いて動作させることができたので紹介したいと思います。 ツールの概要 Apache MXNet Apache MXNet (以下、MXNet)は、 TensorFlow や PyTorch など同じディープラーニングのフレームワークです。 ここ で紹介されているように、AWSのサービスである Amazon SageMaker に利用されています。 GluonCV Toolkit GluonCV Toolkit (以下、GluonCV)は、MXNetの持つパッケージの一つである Gluon をComputer Visionに特化する形で発展させたパッケージです。既知の有名な論文をベースにした学習済みモデルを提供するModel Zooや、新しいデータ拡張の関数などが提供されています。 Model Zooに設置されている学習済みモデルは、 学習スクリプト や その実行コマンド が公開されていて、再現性がある程度担保されるよう運用されています。使い方としては、Gluonベースの学習スクリプトに記述しているモデル部分やデータ拡張部分をGluonCVベースに書き換えるだけで使えて簡単に利用することが可能です。 Amazon SageMaker Neo & DLR Amazon SageMaker Neo はAmazon SageMakerのサービスの一つで TVM Stack や DLR で動作するようにモデルをコンパイルするサービスです。TVMやDLRは、ディープラーニングフレームワークのモデルをGPUやCPUに最適化して実行してくれるツールです。 モデルを動かしてみる 1. GluonCVのModel Zooからモデルを保存する Model Zooにあるモデルを扱うのが簡単なので、以下のスクリプトを使ってモデルをダウンロードしモデル保存します。モデルはtar.gzで圧縮して保存していますが、これはAmazon SageMaker Neoで扱うためです。この記事では、 “ResNet152_v1d” を使って実験を行っていますが Model Zooのサイト の中から別のモデルを選択できます 1 。 import tarfile import argparse import gluoncv import mxnet as mx from pathlib import Path if __name__ == '__main__' : parser = argparse.ArgumentParser() parser.add_argument( '--model-name' , type = str , default= 'ResNet152_v1d' ) args = parser.parse_args() model_name = args.model_name # model_zooからモデルを取得 net = gluoncv.model_zoo.get_model(model_name, pretrained= True ) # モデルを保存するための前準備 net.hybridize() # 画像の入力サイズが違う場合は変更する dummy = mx.nd.ones([ 1 , 3 , 224 , 224 ]) _ = net(dummy) # 保存先ディレクトリの設定と作成 store_dir = Path(f './{model_name}' ) if not store_dir.exists(): store_dir.mkdir() # モデルの保存 net.export(f '{store_dir}/{model_name}' ) # SageMakerNeoでコンパイルできるようにtar.gzで圧縮 tar_name = './' + model_name + '.tar.gz' archive = tarfile.open(tar_name, mode= 'w:gz' ) archive.add(store_dir) archive.close() 2. Amazon SageMaker Neoでコンパイルする 任意のs3のバケットへモデルのtar.gzファイルを配置してAmazon SageMaker Neoでコンパイルをします。以下の画像のように、Amazon SageMakerサービスへログインしてコンパイルを行います。下図のようなGUI操作を行い、コンパイルされたモデルがtar.gzで吐き出されれば完了となります。 Amazon SageMakerのコンパイル設定画面 3. Jetson TX2上のDLRを使って実行する DLRを ここ を参考にインストールしたNVIDIA Jetson TX2上で以下のスクリプトを実行しました。前準備として、下記のスクリプトと同じディレクトリに、 compiled フォルダを作成し、このフォルダ以下にコンパイル済みモデルを格納しておきます。 import sys import numpy as np from dlr import DLRModel import datetime if __name__ == '__main__' : # モデルのロード device = 'gpu' model = DLRModel( 'compiled' , device, 0 ) # 入力画像のサイズを指定 image_size = 224 times = [] # 100回実行する for _ in range ( 100 ): # 入力データをランダムに生成する im = np.random.random([ 1 , 3 , image_size, image_size]) b, h, w, c = np.array(im).shape input_data = { 'data' : im} # Predict start_prediction_time = datetime.datetime.now() out = model.run(input_data) # 処理時間を保持 end_prediction_time = datetime.datetime.now() duration = end_prediction_time - start_prediction_time sys.stdout.write(f " \r prediction time duration: {duration}" ) sys.stdout.flush() times.append(duration.total_seconds()) print () print ( '-------------------------------------------------------' ) print (f 'total_time: {np.sum(times)}' ) print (f 'mean_time: {np.mean(times)}' ) print (f 'median_time: {np.median(times)}' ) print (f 'std_time: {np.std(times)}' ) print ( '-------------------------------------------------------' ) print ( 'finished prediction' ) 実行した結果は以下の画像のようになりました。推論速度は、今回の検証項目の内容ではありませんが試しに取得してみました。だいたい、 1/0.053=18fps ぐらい出ているようです。 ランダム入力による推論結果 また、FP16で推論した結果も載せておきます。おおよそ、中央値でみると1.7倍早くなっているようです。 ランダム入力による推論結果(FP16) まとめ GluonCVのモデルを複数のツールを使ってJetson TX2上で動作させました。引き続き、機械学習に関するやってみたや業務内で検証した内容を紹介していこうと思います。 他のモデルでは試していないので動かない可能性があります。 ↩
はじめに 製品開発グループの野本です。 組込ソフトウェアエンジニアとしてデータ収集用端末のソフトウェア開発を担当しています。 今回は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を実装する皆さんの力になれればという思いで紹介しました。 記事のボリュームの兼ね合いで割愛した部分も多いですが、リクエストいただければ別記事で解説できればと思いますので、お気軽にお問い合わせください!