TECH PLAY

セーフィー株式会社

セーフィー株式会社 の技術ブログ

246

概要 こんにちは!2024年新卒エンジニアの古谷です。今回は技育祭 2024【秋】にエンジニアリングオフィスの副部長の武田さんと登壇してきたことについてお話ししようと思います。 概要 技育祭とは 登壇目的 登壇概要 所感 技育祭とは 技育祭は「技術者を育てる」ことを目的としたエンジニアを目指す学生のための日本最大のオンラインカンファレンスです。各企業の経営者やCTO、エンジニアが二日間に渡って多くの学びや気づきを提供するために登壇します。 登壇目的 私の登壇には2つの目的がありました。 2024年新卒エンジニア研修で身につけた「プロダクト開発で大切なこと」を学生の皆さんに共有し、より引っ張りだこなエンジニアになってもらうこと 私が学生時代にウェブアプリを開発していた時には得られなかった知見をたくさん紹介しました。ハッカソンなどの開発では得られにくい、運用を大前提とした開発における大切なことに、興味を持っていただけると考えました 学生の皆さんにセーフィーがどんなことをしているのか知り興味を持っていただくこと 当日は120人を超える方々に見にきていただき、コメントの反応も良好でした。登壇直後にはカジュアル面談の申し込みが来るなど、学生の皆さんに興味を持ってもらうことができました 登壇概要 以下は登壇した時の資料です。 speakerdeck.com スライドの中では、プロダクトとプロジェクトの違いを説明しつつ、プロダクト開発で大切なことを2024新卒エンジニア研修で作った「isai connect」という異才ランチを活発化させるためのプロダクトを例に説明しています。異才ランチはセーフィーの福利厚生の一つで、他部署の方とランチに行くとランチの費用を会社が負担してくれる制度です。 後半は、私と武田さんによるセッションと学生の方々からの質疑応答を行いました。質問には以下のようなものがありました。 なぜセーフィーに入社したのか プロダクトのアイデアを思いつく良い方法 キャリアビジョンとしてプロジェクトマネージャーを経てプロダクトマネージャーを目指すことはあるか マネージャー職でも積極的にコーディングできるか 就職活動時の最終候補企業 所感 新卒で技育祭に登壇することは大変緊張しましたが、学生時代にお世話になった技育祭に社会人になってからも関わることができ嬉しかったです。 セーフィーでは、様々な挑戦ができる環境があります。この記事を読んでもし興味を持っていただけた方は、ぜひ採用ページもご覧ください。カジュアル面談のみでも大歓迎ですので、お気軽にご連絡ください。 safie.co.jp
こんにちは、第3開発部AI Visionグループのおにきです。 この記事では11月16日(土)にセーフィー社内の会議室で行われたイベント、「 第62回 コンピュータビジョン勉強会@関東 ECCV2024読み会 」について紹介したいと思います。 コンピュータビジョン勉強会@関東 開催にいたる経緯 準備 会場の確認と設備チェック 参加者への配慮 懇親会の準備 当日の様子 参加者の到着 セーフィーの紹介 セーフィーからの発表 その他の発表と全体の印象 懇親会 さいごに コンピュータビジョン勉強会@関東 「 コンピュータビジョン勉強会@関東 」は、takminさんを中心とするメンバーによって14年前から運営されている勉強会です。今回で62回目を迎え、コンピュータビジョン分野では長年にわたり継続している老舗の勉強会として知られています。研究・開発をしているリサーチャー、プロダクトの開発をしているエンジニア、大学で研究を行っている学生など、コンピュータビジョンに関わる多様な方が参加者として集まるレベルの高い勉強会です。 これまで様々なテーマで勉強会は開催されてきましたが、CVPR、ECCV、ICCVと言ったコンピュータビジョンの領域でのトップカンファレンスで発表された論文を解読して紹介するという形式の勉強会は参加者も多く盛り上がるテーマの一つでカンファレンスと時期を合わせて開催されてきました。今回はミラノで行われたECCV(The European Conference on Computer Vision)2024が対象の勉強会となりました。 開催にいたる経緯 私自身もコンピュータビジョンのエンジニアとして、10年以上前から何度かこの勉強会に参加してきました。その中で2回ほど発表の機会もいただき、大変お世話になっています。 夏の勉強会に参加した際、主催者の方々と話をする機会がありました。その中で、セーフィーが1年前にオフィス移転をし、100名収容可能な会議室があるので会場提供できるかもという話になりました。 ちょうどその頃「Mobile Dev Japan #3」という勉強会がセーフィー内で開催されていました。これにより、セーフィー社内でもIT系の勉強会の開催に対して前向きな雰囲気が高まりつつありました。 8月頃にECCV読み会の日程調整と参加者募集が始まったので、会場提供できると連絡したところぜひという回答をいただいたのでセーフィーで開催をすることが決まりました。 せっかく自社のオフィスで開催するということで、AI Visionグループから2名のメンバーを登壇させてもらいました。 準備 会場の確認と設備チェック 今回は土曜日の開催ということもあり、そもそも土曜日はビルのエントランスドアが空いているのかなど入館の仕方についての確認を行いました。 会場となる会議室の設備については、私自身が設定したことがなかったため、前日の金曜日夕方にチェックを行いました。平日開催であれば社内にいる人に助けてもらえそうですが、週末となると他の社員は誰もいないので、入念にチェックは行いました。 参加者への配慮 ECCV読み会は休憩を挟みつつ、午後1時~6時半までの長時間で開催されます。トップカンファレンスの論文の内容を紹介するため、参加者も脳をフル稼働します。セーフィーの入っているオフィスビルは週末になるとコンビニが営業しておらず周囲にもコンビニがないため、せめて飲み物とお菓子を提供しようということで水・お茶とブラックサンダーを用意しました。 お茶と水(ブラックサンダーは休憩時間に提供しました) 弊社製品を触っていただけるデモとグッズとパンフレットも用意しました 初めてセーフィーのオフィスに訪問する方ばかりなので迷わないように案内も設置しました。 懇親会の準備 コンピュータビジョン勉強会@関東では毎回勉強会後に懇親会が開かれ、そちらも盛り上がります。今回はセーフィーからスポンサーとしてお酒と食べ物を提供することにしました。みなさんどれくらいお酒を飲むのかの予測はなかなか難しいですが、適当に一人あたりの本数を決めて発注しました。 当日の様子 参加者の到着 開催時間の30分前から会場に入れるように案内を開始しました。当初は90人近くイベント登録していただいていましたが、実際に入った数は50名弱でした。このあたりのキャンセル者の多さはどの勉強会でも共通の悩みのようです。それでも、50名もの方が実際にセーフィーに来ていただいたということは嬉しい限りです。 セーフィーの紹介 takminさんの挨拶の後に、私から会場提供企業としてセーフィーの会社紹介を行いました。50人近いコンピュータビジョンエンジニアの前でセーフィーを紹介し、実際にプロダクトのデモを見てもらうことでセーフィーという会社とセーフィーでのコンピュータビジョンエンジニアの仕事を認識してもらえたかと思います。発表後にXで確認すると参加者からの反応も良かったようです。 セーフィーからの発表 セーフィーからは周さんと橋本さんに登壇してもらいました。 周さんの発表 Li et al., ”VideoMamba: State Space Model for Efficient Video Understanding” Video Understandingタスクのために最新のアーキテクチャであるMambaを応用した論文です。Deep Learning、Transformerに次ぐ新しいアーキテクチャとしてのMambaを説明した後に、Video Understandingタスクに応用するという内容で数理的にかなり複雑なものでしたが、最新のアーキテクチャということもあり参加者の興味は高かったようです。 speakerdeck.com 橋本さんの発表 Ciortan et al. , “Minimalist Vision with Freeform Pixels” CNNの畳み込み演算を光学系で行うことで、計算機での演算を軽量化するという論文です。ECCV2024のBest Paperを受賞した論文で、発想が非常にわかりやすい研究という印象でした。橋本さんは実際に学習をしてみたそうですが上手くいかなかったようですが、実際に手法を試してみるというのは大切な姿勢だと再認識しました。 speakerdeck.com その他の発表と全体の印象 セーフィーからの2名を含め、合計11名の方が論文の紹介を行いました。どの資料もわかりやすく、また質疑も発表者の方への敬意があるやり取りになっており、本当に良い勉強会コミュニティーであると感じました。 コンピュータビジョンと言ってもその扱う領域が幅広いことと、いろいろな発想のもとに研究が行われているということを知ることができ、非常によい刺激を受けることのできる勉強会でした。 懇親会 勉強会後、ネットワーキングを目的とした懇親会を開催しました。参加者は飲食を楽しみながら、それぞれの会社での業務の進め方やコンピュータビジョン分野での業務上の課題など、普段社内で業務を知ることのできない内容について情報交換をすることができました。このように社外の方と交流することで普段考えている課題などを共有したり、他社での状況を知ることができます。それらは日々の業務のヒントにつながっていくと思われるので、今後も積極的に行っていきたいと考えています。 さいごに 今回の勉強会は週末開催で参加者も多く準備が大変でしたが、チーム内外のメンバーの協力のおかげで開催することができました。開催前は不安なことも多々ありましたが、大きな問題もなく無事開催することができました。スタッフとして参加したセーフィーのメンバーからも楽しかったし勉強にもなったという声を聞いており、開催して本当に良かったと感じています。 セーフィーでは今後もコンピュータビジョンを用いた画像認識AIの開発を進めて行くので、一緒に開発を行うメンバーを募集しています。 open.talentio.com
概要 AIチャットツール導入の目的 LLMおよびAIチャットツールの選定 機密情報・個人情報とLLM LLMの比較表 AIチャットツールの比較表 Bedrock Claude Chat AIチャットボットの導入 Slack連携機能 まとめ 概要 近年大規模言語モデル (LLM) 技術の発達に伴い、社内でもGitHub Copilotをはじめとする各種AIツールの導入が進んでいます。 本件ではLLM技術を用いたAIチャットツールとしてAmazon Bedrockおよび Bedrock Claude Chat を導入した経緯について紹介します。 AIチャットツール導入の目的 2022年末にOpenAI社よりChatGPT 3.5がリリースされて以降LLM技術およびAIチャットボットが急速に普及し、様々な分野で活用されるようになりました。 弊社でもGitHub Copilotによるコード生成補助などでAI技術の導入による社内業務効率化が進められており、また生成AIと映像を組み合わせた新たなサービスの創出が進んでいます。 一方でAIチャットサービスについては社内機密情報・個人情報を扱わない範囲で個別に使用することが認められているだけで、本格的な社内業務改善に用いるのには難しい状況でした。 そこで、社内業務効率化のため、あるいはより高度なAIツール導入の足がかりとするため、また生成AIを用いたサービス創出のための経験値を積むために社内AIチャットボットを導入することにしました。 LLMおよびAIチャットツールの選定 LLMおよびAIチャットツールを選定するにあたり下記の点を考慮しました。 LLMの提供においてはサービスプロバイダが入力を再学習のために使用したり不正使用レビューのため閲覧することが多いため、機密情報・個人情報の扱いについては通常のSaaSサービスと比べ追加の検討が必要になります。 LLM性能 同世代のLLMと比較して十分な性能を持つこと AIチャット機能 ユーザーごとのチャット履歴の保存ができること 検索拡張生成 (RAG) による社内ナレッジに基づくAIチャットボットの提供 その他高度な機能 (Agent、外部ツールアクセス等) 管理機能 SSO ユーザーごとの費用の確認 コンプライアンス LLMの入力が再学習に使用されないかオプトアウトが可能であること LLMの入力が人間のレビューに使用されないかオプトアウトが可能であること 費用 安価であること 機密情報・個人情報とLLM SaaSとしてLLMまたはAIチャットサービスを使用する場合、個人情報を入力することは第三者提供にあたり、ユーザー本人の同意が必要になります。 一般にGMail, Google Drive等クラウドサービスで個人情報を扱うには、対象サービスがいわゆるクラウド例外 ( 個人情報の保護に関する法律についてのガイドライン Q7-53 等) に該当する必要があります。 LLMへの入力が再学習など出力の生成以外の用途に用いられる場合、クラウド例外に該当しません。( https://www.ppc.go.jp/news/careful_information/230602_AI_utilize_alert/ ) またLLMへの入力が不正検知などのために人間によるレビューが行われる場合、クラウド例外に該当しなくなるという見解があります。 【AI】生成AIとクラウド例外 本件では再学習および人間によるレビューが行われないことを条件に、機密情報 (社内秘情報・機密保持契約にカバーされるものを除く) および個人情報の扱いが可能な社内AIチャットボットの導入を進めました。 LLMの比較表 名称 Chatbot Arena Score 再学習 レビュー OpenAI ChatGPT GPT-4o-2024-08-06 1264 有り (オプトアウト可) 人力レビュー (オプトアウト申請可) OpenAI ChatGPT API ChatGPT-4o-latest 1338 無し (オプトイン) 人力レビュー (オプトアウト申請可) Anthropic Claude Claude 3.5 Sonnet 1268 有り (オプトアウト可) 人力レビュー (オプトアウト申請可) Anthropic Claude API Claude 3.5 Sonnet 1268 有り (オプトアウト可) 人力レビュー (オプトアウト申請可) Google Gemini Gemini-1.5-Pro-002 1304 有り 人力レビュー Azure OpenAI GPT-4o-2024-08-06 1264 無し 人力レビュー (オプトアウト申請可) Amazon Bedrock Claude 3.5 Sonnet 1268 無し 自動レビュー ※ Chatbot Arena Leaderboard (2024-10-14) より ※ 不正検知レビューのオプトアウトは申請が受諾される必要があります AIチャットツールの比較表 提供方法 対応モデル SSO カスタムプロンプト RAG Reasoning 外部ツール コンテンツ編集 費用 OpenAI ChatGPT SaaS ChatGPT ChatGPT Enterprise (SAML) GPTs GPTs o1-mini Bing, DALL-E, Code Interpreter, Retrieval, Custom Actions Canvas >= $30/user/month (Enterprise) Anthropic Claude SaaS Claude Claude Enterprise Projects Projects - - Artifacts >= $30/user/month (Enterprise) Google AI Studio SaaS Gemini Google Workspace - - - - - 従量課金 Bedrock Claude Chat セルフホスト (UI) + SaaS (LLM) Bedrock (Claude等) Amazon Cognito (SAML/OIDC) BOT BOT ReAct DuckDuckGo - インフラ料金 + API従量課金 Bedrock Claude Chat Bedrock Claude Chat はAmazonによって開発されているチャットボットUIで、Amazon Bedrock基盤モデルによりAnthropic Claude等を利用可能です。ユーザーはCDKを使用して自身のAWS環境にソフトウェアをデプロイすることができます。 Bedrock Claude Chatは必要な条件を満たし、費用がSaaS製品と比べ非常に安価であるためこれを導入しました。 LLM: Anthropic Claude 3.5 Sonnetが使用でき、選定当時での最高性能 AIチャット: ユーザーごとのチャット履歴の保存、RAG等必要な機能が使用可能 SSO: Amazon CognitoによるSSOが使用可能 コンプライアンス: 再学習および人間によるレビューが行われない 費用: 比較的安価なLLM APIが使用でき、インフラ費用が非常に安価 (月額数百ドル程度) Bedrock Claude Chat 画面イメージ AIチャットボットの導入 Bedrock Claude Chatをデプロイし、全社利用を開始しました。 ユーザーはSSOでアプリにログインし、一般的なAIチャットボットとして使用することができます。 またユーザー権限でボットを作成することができ、カスタムプロンプトの設定や外部URLまたはドキュメントを知識ソースとしてRAG機能を構成し、共有することができます。 現在セーフィー製品の仕様のQ&Aボットなどいくつかの公開ボットが稼働しています。 Slack連携機能 Bedrock Claude ChatにはAPI公開機能があり、こちらを利用してSlackとの連携機能を開発しました。 suzuki-safie/slack-chatbot-fn SlackのCustom FunctionとしてBedrock Claude ChatのBotの公開APIを呼ぶことで、Slack上からチャットボットの機能を利用することができます。 まとめ 社内AIチャットボットとしてBedrock Claude Chatを全社導入し、社内業務改善に使用可能なツールを安価に導入することに成功しました。この導入により、AIを活用した業務効率化の基盤が整いました。 今後は以下の取り組みを通じて、AIチャットボットの活用をさらに推進していく予定です: 利用方法の周知と活用促進 RAG(検索拡張生成)などの高度な機能の使用方法について、社内トレーニングや事例共有を実施 ユーザーの利用体験を向上させ、AIチャットボットの日常的な活用を促進 開発用途での利用拡大 GitHubのコードリポジトリをソースとしたRAGの構築 生成AIを用いた新サービスの創出 AIチャットボット以外の用途でのBedrock基盤モデルの活用 社内アプリケーションやサービスへのAI機能の組み込み これらの取り組みを通じて、AIテクノロジーの社内での浸透を図り、業務効率の向上だけでなく、新たなサービスの創出にもつなげていきます。
はじめに セーフィー株式会社 の AI Vision グループでテックリードを務めます橋本貴博です。 私たちのチームでは、レビューの属人化や特定のメンバーへの負担の集中が課題となっていました。どのメンバーがレビューするかによって、そのやり方やフィードバックの質にバラつきが出ることも少なくなく、これがコードの品質に影響を及ぼしていました。 そのため、チーム全体でレビューを行うことで、コードの知識を全員で共有し、レビューのやり方や品質を標準化したいと考えていました。こうすることで、誰がレビューしても同じように高品質なコードが保たれるようになります。 さらに、Pull Request (PR) の数をKPIとして考え、それに基づいて実装を推進することも重要視しました。PRの数を定量的な指標にすることで、チームの成果を把握し、開発の効率化を図りたいと考えました。 そこで、GitHubのPRを解析して可視化するツールを開発し、オープンソースで公開することにしました。このツールを使うことで、PRの状況を簡単に把握できるだけでなく、レビューの分散や効率化にもつながると考えています。 本記事では、開発したツールの機能と活用、利用している GitHub REST API、ツールの使い方について示します。 はじめに 機能 積み上げ縦棒グラフ ヒートマップ ネットワークグラフ サンキーダイアグラム GitHub REST API Search API Reviews API Review Requests API 使い方 ソースコード むすび 機能 特定の期間において、指定されたメンバーが author、reviewer、もしくは requested review されているPRの数を集計します。結果を可視化して png 画像として出力します。出力される図は以下の通りです。 積み上げ縦棒グラフ ヒートマップ ネットワークグラフ ノードの大きさ: PR作成数とレビューした数の合計 エッジの太さ: レビューした数と、レビューされた数の合計 サンキーダイアグラム 以下では、2024年9月の1か月のPRを集計した結果を例として説明をします。 積み上げ縦棒グラフ Author はPRを作成した数です。極端にPR数が少ない場合、実装に時間がかかっていたり、実装以外の業務の割合が多くなっているといった理由が考えられるので注意しています。 Review-requested は現時点でレビュー依頼されている数です。レビュー依頼が特定のメンバーに溜まっている場合は再割り当てが必要な可能性があるので注意しています。 Review-completed は、レビューが完了している数です。PRに対して1回以上のレビュー(comment, approve, request changes)を行った数から、完了していない場合を除くため、現時点でレビュー依頼されている数を除いています。私たちのチームでは特定のメンバーにレビューが片寄ることを防ぐため、可能であればレビュー数が少ないメンバーにレビュー依頼を行うことを推奨しています。 ヒートマップ 積み上げ縦棒グラフは、各メンバーがレビューした総数は分かりますが、レビュー依頼元の内訳を読み取ることはできません。そこで、依頼元と依頼先の関係を可視化するためにヒートマップを出力するようにしました。ヒートマップの縦軸は依頼元、横軸は依頼先を示しています。PR作成者によっては、依頼先が偏りレビュー属人化の傾向があることが読み取れると思います。 ネットワークグラフ ネットワークグラフは、ヒートマップの情報をさらに直感的に分かりやすく可視化することができます。ノードの大きさは各メンバーのPR作成数とレビュー数の合計から、エッジの太さは2人のメンバーの間でレビューした数とレビューされた数の合計から決定しています。 ノードの位置は Spectral Layout を用いています。これにより結びつきが強いノードは近くに配置されます。したがって、コードやレビューの知識共有が近接するメンバーで密に行われており、逆に離れたメンバにはあまり共有が行われていないのではないかと仮説を立てることができます。 サンキーダイアグラム サンキーダイアグラムは、ある量とその流れを可視化します。ここでは、レビュー依頼の量とその流れを見ることができます。下図において、左端の列はレビューの依頼元と依頼の量を、右端の列はレビューの依頼先と依頼された量を示しています。依頼元から依頼先への流れがグレーの帯で示されています。サンキーダイアグラムを用いると、直感的に誰から誰へどれくらいの量が分配されたのか把握できると思います。 GitHub REST API ここでは、GitHub REST API を用いたPR情報の取得方法について説明します。このツールでは3種類のAPIを利用しています。 まず、 REST API for search (Search API) を用いて集計に含めるPRのリストを取得しています。次に、個々のPRについて、 REST API endpoints for pull request reviews (Reviews API)と、 REST API endpoints for review requests (Review Requests API)を用いて、レビューのステータスとレビュー依頼のステータスを取得しています。 集計の高速化のため、過去に取得した時点から更新が無いリポジトリについてはキャッシュを利用しています。 Search API Search API は、 github.com/search と同様のクエリを用いてGitHub 内のオブジェクトを検索することができます。例えば、特定の期間、特定の組織で、特定のメンバが作成したPRを検索するには検索窓に以下のクエリを入力できます。 type:pr org:Safie author:member-a author:member-b author:member-c created:2024-09-01..2024-09-30 上記に対応して、REST API のURLは次のようになります。 https://api.github.com/search/issues?q=type:pr+org:Safie+author:member-a+author:member-b+author:member-c+created:2024-09-01..2024-09-30&sort=created&order=desc&per_page=100 Reviews API レビューしたメンバーを確認するのに Reviews API を用います。リポジトリのオーナーを {owner} 、リポジトリの名前を {repo} 、PRの番号を {pull_number} として、URLは以下になります。 https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/reviews Review Requests API 現時点でレビュー依頼されているメンバーを確認するのに、Review Requests API を用います。Reviews APIと同様に、リポジトリのオーナーを {owner} 、リポジトリの名前を {repo} 、PRの番号を {pull_number} として、URLは以下になります。 https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers 使い方 config.py に GitHub token と集計したいメンバーのアカウント名を記載します。 github_token = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" authors = [ "member-a" , "member-b" , "member-c" , ] 以下のコマンドを実行します。 python analyze.py # 直近1か月の集計 python analyze.py --from_date 2024 -09- 01 --to_date 2024 -09- 30 # 期間の指定 詳細な使い方は、リポジトリの README.md をご覧ください。 ソースコード こちら でソースコードを公開しています。ぜひ使ってみてください。 github.com むすび GitHub REST API を使ってPRを集計し、PRの属人化やチームの生産性の分析に利用する方法を紹介しました。もし同じような課題に直面しているチームがいれば、ぜひこのツールを活用してみてください。 セーフィーでは、新卒・中途を問わず、さまざまな職種の仲間を積極的に募集しています。詳細は セーフィー採用サイト をご覧ください。
こんにちは!Safie第2開発部のAndroidエンジニアのジェローム( @yujiro45 )です。 Androidで3D/ARモデルを表示するのは難しそうに見え、技術的に大変そうですね。 この記事では、 Sceneview-android を使用して簡単に3DとARモデルを表示する方法を紹介します。 SceneViewとは 導入 実装 アニメーション ARで動画を表示する まとめ SceneViewとは SceneView は Google Filament と ARCore を使用して3Dモデルを表示するライブラリです。 Thomas Gorisse (トーマス・ゴリス)と彼のチームによって開発されました。 Google Filament :Googleが開発している物理ベースのリアルタイムレンダリングエンジンです。 ARCore :Googleが開発したAndroid端末向けのARフレームワークです。 SceneViewは、以前のライブラリである Sceneform のKotlinバージョンであり、完全にKotlinで書き直されています。Sceneformはもはやメンテナンスされていません。 技術的には、SceneViewは3Dスペースで、カメラは中央に位置しており、座標はx=0、y=0、z=1です。 該当部分のコード 導入 この記事では、KotlinとJetpack Composeを利用する導入方法について説明します。ただし、SceneViewはAndroidのxml layoutや Flutter や React Native でも利用可能です。 SceneViewは2つのビューを提供します。 ARSceneView : Google FilamentとARCoreの両方を使用して、3DおよびARを表示する目的で利用されます。 SceneView : Google Filamentを使用して3Dモデルを表示するために利用されます。 SceneViewを追加するには、必要に応じてGradleファイルに適切な依存関係を追加するだけです。 ARSceneView dependencies { implementation( "io.github.sceneview:arsceneview:X.X.X" ) } SceneView dependencies { implementation( "io.github.sceneview:sceneview:X.X.X" ) } 💡 GradleファイルにARSceneViewの依存関係を追加すると、ARSceneViewとSceneViewの両方を使用できます。ただし、SceneViewの依存関係のみを追加した場合は、SceneViewのみを使用できます。 実装 SceneViewは現在、GLTFおよびGLBファイルをサポートしています。3Dモデルのファイルをres/rawフォルダに入れてください。3Dモデルを表示するには、 ModelNode を使用する必要があります。これにより、3Dモデルから3Dノードを作成することができます。 *glbとgltfファイルは以下のサイトで無料でダウンロード出来ます。 https://www.cgtrader.com/ https://polyhaven.com/ https://sketchfab.com/tags/glb 3Dファイルを追加したら、以下のコードを使用して表示できます。 ARSceneView SceneviewexampleTheme { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) // 3Dモデルの読み込み val modelNode = ModelNode( modelInstance = modelLoader.createModelInstance(R.raw.android) ).apply { // 3Dモデルの初期設定 scale = Scale( 1 / 40f ) position = Position( 0f ) } ARScene( modifier = Modifier.fillMaxSize(), engine = engine, modelLoader = modelLoader, childNodes = rememberNodes { // モデルノードの追加 add(modelNode) } ) } SceneView SceneviewexampleTheme { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) // 3Dモデルの読み込み val modelNode = ModelNode( modelInstance = modelLoader.createModelInstance(R.raw.android) ).apply { // 3Dモデルの初期設定 scale = Scale( 1 / 40f ) position = Position( 0f ) } Scene( modifier = Modifier.fillMaxSize(), engine = engine, modelLoader = modelLoader, childNodes = rememberNodes { // モデルノードの追加 add(modelNode) } ) } ビルドが通ったら、3Dモデルが表示されました! 🎉 ARSceneView SceneView 出典:Sketchfab, https://sketchfab.com/3d-models/android-7c30eda007684abbb78ea4b99d22fc2c  (2024/10/07アクセス) SceneViewにノードを追加するとき、 ドキュメント によれば、ノードのデフォルトの位置は (x=0.0f, y=0.0f, z=0.0f) で、軸は下の画像のようになります。 カメラを別の位置に移動したい場合は、以下のように指定します。 SceneviewexampleTheme { //... Scene( //... cameraNode = rememberCameraNode(engine).apply { position = Position(z= 5f ) } //... ) } アニメーション ModelNodeのtransform()というメソッドを使用して、位置や回転やScaleを変更することができます。 https://github.com/SceneView/sceneview-android/blob/1e303adf92f1f7b67eb9aa4f9fc731d75213ac97/sceneview/src/main/java/io/github/sceneview/node/Node.kt#L620-L633 fun transform( position : Position = this .position, quaternion : Quaternion = this .quaternion, scale : Scale = this .scale, smooth : Boolean = isSmoothTransformEnabled, smoothSpeed : Float = smoothTransformSpeed ) = transform(Transform(position, quaternion, scale), smooth, smoothSpeed) 前のコードでは、3Dモデルを表示する事が出来ましたので、モデルをタップしたときに180度回転させるようにしましょう。 ARSceneView SceneviewexampleTheme { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) // 3Dモデルの読み込み val modelNode = ModelNode( modelInstance = modelLoader.createModelInstance(R.raw.android) ).apply { // 3Dモデルの初期設定 scale = Scale( 1 / 40f ) position = Position( 0f ) } ARScene( modifier = Modifier.fillMaxSize(), engine = engine, modelLoader = modelLoader, childNodes = rememberNodes { // モデルノードの追加 add(modelNode) }, // タッチイベントのリスナー onGestureListener = rememberOnGestureListener( onSingleTapConfirmed = { _, node -> if (node == modelNode) { // モデルノードの回転 modelNode.transform( rotation = Rotation(z = 180f ), smooth = true ) } } ) ) } SceneView SceneviewexampleTheme { val engine = rememberEngine() val modelLoader = rememberModelLoader(engine) // 3Dモデルの読み込み val modelNode = ModelNode( modelInstance = modelLoader.createModelInstance(R.raw.android) ).apply { // 3Dモデルの初期設定 scale = Scale( 1 / 40f ) position = Position( 0f ) } Scene( modifier = Modifier.fillMaxSize(), engine = engine, modelLoader = modelLoader, childNodes = rememberNodes { // モデルノードの追加 add(modelNode) }, // タッチイベントのリスナー onGestureListener = rememberOnGestureListener( onSingleTapConfirmed = { _, node -> if (node == modelNode) { // モデルノードの回転 modelNode.transform( rotation = Rotation(z = 180f ), smooth = true ) } } ) ) } 結果を見てみましょう! ARSceneView SceneView ARで動画を表示する 3Dモデルの表示方法がわかったので、以下の動画をExoPlayer x Sceneviewで表示できるように実装していきましょう! こちら で無料動画を入手できます。 出典:Pexels, https://www.pexels.com/ja-jp/video/13299023/  (2024/10/07アクセス) Sceneviewのソースコードを見ると、 VideoNode が無効になっていることがわかりました。 また、Nodeのテクスチャを変更するには、Google Filamentの MaterialInstance を作成し、Sceneviewの MaterialLoader を使用して新しいテクスチャを適用する必要があります。そのためには、以下の手順を実行する必要があります。 こちらがコードです!✨ class ExoPlayerVideoMaterial( engine : Engine, exoPlayer : ExoPlayer, private val materialLoader: MaterialLoader, ) { // SurfaceTexture作成 private val surfaceTexture = SurfaceTexture( 0 ).apply { detachFromGLContext() } // SurfaceTexture→Surface作成 private val surface = Surface(surfaceTexture) // Textureのために、FilamentのStream作成 private val stream = Stream.Builder() .stream(surfaceTexture) .build(engine) // Texture作成 private val texture = VideoTexture.Builder() .stream(stream) .build(engine) // VideoTextureを使ったMaterialInstance作成 val videoInstance get() = materialLoader.createVideoInstance(videoTexture = texture).apply { setExternalTexture(texture) } init { // ExoPlayerのSurface設定 exoPlayer.setVideoSurface(surface) } } Sceneview側では、作成した ExoPlayerVideoMaterial を使いましょう! SceneviewexampleTheme { // ExoPlayerの設定... val player = ExoPlayer ... // さっきの作成したExoPlayerVideoMaterial val videoMaterial = ExoPlayerVideoMaterial(engine, player, materialLoader) val videoNode = PlaneNode( engine = engine, // 動画は16:9のため size = Size( 16f , 9f ), // NodeのMaterialInstance設定 materialInstance = videoMaterial.videoInstance, ) ARScene( //... childNodes = rememberNodes { // ノードの追加 add(videoNode) }, ) } 結果を見てみましょう! ARで動画を表示出来ました!😀 まとめ Sceneviewは、3DやARコンテンツを表示するための非常に便利なライブラリです。設定や使用がとても簡単です。しかし、私の個人的な経験から言うと、ドキュメントがあまり多くないため、もう少し複雑なことをしようとすると、Githubのソースコードを見る必要があります。アプリに3Dを追加したい場合は、ぜひ試してみてください! モバイルチームは開発する仲間を募集しています! open.talentio.com
こんにちは!Safie第2開発部のAndroidエンジニアのジェローム( @yujiro45 )です。 今年も、日本最大のAndroid開発者イベントであるDroidKaigi 2024に参加してきました! Day 0のワークショップから最後まで参加しました。この記事では、今年10周年となるDroidKaigiで何をやったのか、何があったのか、個人的におすすめのトークなどについてお話しします! Day 0 - Workshop Day 1 & 2 - Event Booths Sessions Best sessions Compose UIを使ったクリエイティブで複雑なユーザーインターフェース AndroidのMediaPipeによる画像認識と物体検出 Android StudioのGeminiでコーディングの生産性を高める まとめ Day 0 - Workshop JetbrainsのエンジニアSebastian Aignerさん( @sebi_io )はKotlin Multiplatform (KMP)とCompose Multiplatformのワークショップをやってくれました! 出典:DroidKaigi,X, https://x.com/DroidKaigi/status/1833720183393059289  (2024/10/07アクセス) 個人的には、KMPについてあまり知識がありません。いくつかの記事を読んだり、小さな個人プロジェクトで少し試したりしただけで、深くは掘り下げていませんでした。このワークショップは、もっと学ぶ良い機会となりました! ワークショップに参加する前に、開発環境を準備する必要がありました。 ワークショップのリポジトリ をクローンする Fleet (JetBrainsの新しい統合開発環境 (IDE) )をインストールする リポジトリのすべてのプロジェクトをビルドして、正常に動作するか確認する ワークショップは、Kotlin Multiplatformとそのアーキテクチャについての説明から始まりました。KMPの説明はこの記事の目的ではないので、もっと知りたい方は、 こちら のワークショップのスライドをご覧ください。😇 ワークショップでは、3つの課題がありました。 プラットフォーム毎のネイティブAPI(WebのLocalStorage、デスクトップのJava Properties、AndroidのSharedPreferences)を使用して、ローカルに文字列を持続させる関数を作成しました。 二つ目の課題は、Ktorを使用して、画像を取得して表示するためのシンプルなHTTPリクエストを作成しました。 最後の課題は、Jetpack Composeを使用してUIを作成し、リソースフォルダーに置いた画像を表示しました。 ワークショップは難しくはありませんでしたが、Kotlinで異なるプラットフォームのネイティブAPIをどのように使うかや、Jetpack Composeをモバイルアプリ以外でどのように活用するかを学ぶのはとても面白かったです。 Day 1 & 2 - Event Booths Free ice cream 今年は外が暑かったのと、DroidKaigiの10周年を記念して、初めて無料のアイスクリームがありました! Stamp Rally 今年もスタンプラリーがありました。各ブースにはスタンプがあり、必要な数のスタンプを集めると景品がもらえます。今年も全てのスタンプを集めることができました :) Nail art ネイルアートもありました。個人的にネイルアートには興味がありませんが、それが非常に人気がある理由はなんとなく理解できます 😀 出典:DroidKaigi,X, https://x.com/DroidKaigi/status/1833777502004916330  (2024/10/07アクセス) Sessions Best sessions 今年も多くの面白いセッションがあり、順位をつけるのが難しいですが、DroidKaigi 2024での私のお気に入りのトークをご紹介します! 興味があれば、すべてのVODは こちら で見ることができます。 Compose UIを使ったクリエイティブで複雑なユーザーインターフェース このトークでは、ChrisさんがJetpack Composeを使って、ゲームのUIのような非常に興味深いUIデザインを作成することができるのを示しました。Persona 5のUIを例として使用しました。 実際、Jetpack Composeを使って、もっと創造的になろうと努力すべきです。 出典:DroidKaigi, Youtube, DroidKaigi 2024 - [EN] Creative and complex user interfaces with Compose UI | Chris Horner (2024/10/07アクセス) Jetpack Composeがどれほど強力かを実感しました。 Slides : https://speakerdeck.com/chrishorner/creative-uis-with-compose-droidkaigi-2024 デモアプリ: https://x.com/chris_h_codes/status/1835495634335646042 Repository: chris-horner/persona-im AndroidのMediaPipeによる画像認識と物体検出 このトークでは、 MediaPipe をAndroidで使ってオブジェクトの検出や分類を行う方法を見ることができて、とても興味深かったです。MediaPipeはクロスプラットフォームであり、Pythonでも使えるので、個人プロジェクトで試してみたいと思います! 出典:DroidKaigi, Youtube, DroidKaigi 2024 - [EN] Image Recognition and Object Detection with MediaPipe on Android | Anant Chowdhary (2024/10/07アクセス) Android StudioのGeminiでコーディングの生産性を高める このトークは、2人のゲストによって行われました。Android StudioのシニアプロダクトマネージャーであるAdarsh Fernando( @adarshfernando )と、UXデザインリードのChris Sinco( @csinco )です。 Android Studioでは、Copilotは一番人気だと思いますが、GeminiがAndroid Studioでどのように使われるかを見るのは興味深かったです。Geminiによるすべての変更履歴を確認できたり、ユニットテストのシナリオを生成できたりするのは、とてもよかったです。 出典:DroidKaigi, Youtube, DroidKaigi 2024 - [EN] [Guest Talk] Increasing coding productivity with Gemini in Android Studio | Adarsh Fernando, Chris Sinco (2024/10/07アクセス) まとめ 今年もDroidKaigiはとても良かったです。毎年参加者が増えていることが、このイベントの成功を物語っています。多くの優れたエンジニアと出会い、たくさんの人と話し、面白いセッションに参加しました。 来年の開催をもう楽しみにしています! Safieのブースで会いましょう! 😉 モバイルチームは開発する仲間を募集しています! 一緒にDroidKaigiに参加しましょう! open.talentio.com
こんにちは、第2開発部の池田です。 この記事では、2024/07/26 にセーフィー本社で行われたイベント、 Mobile Dev Japan #3 について紹介したいと思います。セーフィー初の(主催ではないですが)自社にて開催された技術系イベントとなります! Mobile Dev Japan 準備〜設営 イベント本番 Safie Viewer Android & SceneView Beginners Guide to SwiftData A History of JavaScript on Mobile Introduction to Kotlin Multiplatform Networking and Q&A 雑感(英語力など) おわりに Mobile Dev Japan mobiledev.jp Mobile Dev Japan は、おもに日本在住の英語話者が参加しているモバイルエンジニアのコミュニティです。Slackでの交流とオフラインのイベントをメインに、精力的に活動されています。English speaker といっても皆さん日本で生活しているので日本語が達者な方も多いですし、その Background はさまざまです。興味を持たれた方はぜひコミュニティのSlackに参加してみてください。 英語に自信がない方でも、 We are mainly speaking English however everyone is welcome to join even if not confident in their English level yet! とのこと! そんなコミュニティが主催しているイベントをセーフィーで開催することになったのは、 第1回のイベント にて当社の Android エンジニア、Jeromeさんが登壇したことがきっかけでした。そこの繋がりでオーガナイザーからイベント会場の候補としてセーフィーにお声が掛かり、第3回イベントの会場提供と飲食のスポンサーをさせていただくことになりました! 準備〜設営 セーフィーでは販売代理店様向けの営業セミナーなどは頻繁に行われており、 PyCon JP などの技術イベントでスポンサーにもなっていますが、自社を会場とするオフラインの技術カンファレンスは初の試みでした。ゲストの入館方法をはじめとして色々と不慣れな準備に苦労しましたが、今回で知見と型ができたので、今後は積極的に自社イベントも開催していきたいと考えています! セミナールーム。わりと無機質です 手作り案内板 ビールとスナック、サンドイッチも準備 Safie Viewerでストリーミング もちろん弊サービスの対応カメラSafie Oneを使って、社内にLIVEストリーミングもさせていただきました。 conpassでのイベント公開、LT枠も早々に埋まり、いよいよイベント当日です。 ビールとスナックも準備できました! イベント本番 オープニングトーク 約30人のゲストを無事に迎えてイベント開始となりました。もちろん英語で進行します。 発表ごとに活発に質問や軽口が飛び交い、リラックスしつつも盛り上がった雰囲気。 途中、ふらりと覗きにきた営業メンバーも「普段のセミナーと全然違いますね。。」と、感心した様子でした。 以下、簡単に登壇の内容を紹介したいと思います。 Safie Viewer Android & SceneView speakerdeck.com まずはセーフィーから、筋肉自慢のAndroidエンジニア、Jeromeさんのスポンサートーク。 今年リリースされたばかりのセーフィーの新サービス、360度カメラのストリーミングにおける3Dデワープ表示について、 Android 版 Safie Viewer の実装内容紹介です。おもに、3D/AR 機能を提供する SceneView ライブラリを利用して、Safie Viewer の映像を 3D モデルにマッピングして動かす具体的な実装手法やハマったポイントなどを紹介しました。 github.com Beginners Guide to SwiftData speakerdeck.com こちらもセーフィーから、今年3月に入社した iOS エンジニアの Adam さんの発表です。 iOS17 から導入された SwiftData について、その基本的な内容と実装方法を紹介しています。 developer.apple.com Adam さんは軽妙なMCでスポンサー紹介のトークも行ってくれた他、社内でも Safie English School と題した英語力の向上イベント(という英語での飲み会)を行ってくれています! A History of JavaScript on Mobile speakerdeck.com Jamie Birch さんの発表。 アプリの話ではなく、モバイル環境におけるJavaScriptエンジンの歴史についてという異色のトーク。個人的にはPSPや各種ガラケーにもバンドルされていたNetFrontが懐かしかったです。。 Introduction to Kotlin Multiplatform Yuya Haradaさんの発表。 クロスプラットフォーム技術Kotlin Multiplatform(KMP)を軸に、自社でのSwiftUI+KMPというスタックの紹介、その利点とチャレンジ部分などについて語られました。 Networking and Q&A 懇親会も活況 数件の飛び入りLT、イベント告知などの後、Networking(懇親会)の時間となりました。 「みんな意外と飲まないですよ」というオーガナイザーのアドバイスで控えめにしたビールの本数が足りなくならないか、少しハラハラしていましたが何とかちょうど良い感じに収まりました。ここで反省としては、ビールにしろチューハイにしろ缶デザインを見ても中身がわからず迷ってる人が多かったので、ドリンク置き場にも何かキャプションを付ければよかったなというところです。お酒もソフトドリンクもよく出た中で、ノンアルコールビールは一本も手に取られませんでした。なるほど。。 懇親会も非常に盛り上がり、またスポンサートークに挟んだ We are Hiring! のスライドから、一部の方には採用にも興味を持っていただけました。 雑感(英語力など) engineers.safie.link engineers.safie.link セーフィーでは今年から本格的にタイ・ベトナムでのビジネスがスタートし、サービスの国際化対応も当然のものとして実装が前提になってきました。また異才一体のカルチャーが示すように、開発本部スタッフにもアメリカ・フランス・ブラジル・韓国・中国といったさまざまなバックグラウンドのメンバーが増え続けています。 一方で、そういったメンバーのほとんどは日本語が堪能ですし、Slackのやり取りや社内ドキュメントはほぼ日本語。まだまだアウトプットの機会が足りないとも思います。昨今AI翻訳がずいぶん賢くなったおかげで英語の読み書きで苦労することはかなり少なくなりましたが、瞬発力を要するこのようなイベントでは自分の英語力にまだまだ至らなさを感じます(要は懇親会であまり話せず凹んだやつですね。。まぁこれは英語力というよりそもそもの会話力というか。ぶつぶつ。。) なので!社内外にたくさんあるチャンスを活かして、このような場をさらに楽しめる英語力の向上を続けていかなくては、という雑感でした。 engineers.safie.link がんばろう。 おわりに 今回は Mobile Dev Japanコミュニティと共催したイベントについての紹介でした。おかげさまで参加者にも好評いただけたようで良かったです。 モバイルチームでは、このように多様な環境で世界に向けたアプリをともに開発する仲間を募集しています! open.talentio.com セーフィーでは今後もこのような技術交流の機会を増やしていきたいと考えています! Thanks for reading! See you next time❤️
こんにちは、フロントエンドエンジニアの阿部です。 弊社では今後のグローバル進出を見据えて、今年から海外への事業を展開しています。 その一環としてSafieViewer・マイページの国際化対応を行いました。 言語は日本語、英語、ベトナム語、タイ語の4言語に対応しており、現段階では主要な機能においてタイムゾーンにも対応しています。 今回はWebフロントの実装に焦点を当て、SafieViewerとマイページの国際化対応について振り返りたいと思います。 なお、Angularフレームワークを使用しているため、実装の説明はAngularに依存します。 多言語対応とタイムゾーン対応 多言語対応 辞書の作成 メリット デメリット 注意点 翻訳サービスの実装 実装 特殊な翻訳パターンへの対応 翻訳するテキストに変数が入る場合 複数形対応 Serviceで実装することの問題点 Angular外では利用できない 野良関数ではDIができない クラスの外だとDIできない 解決方法 タイムゾーン対応 タイムゾーンの取得 タイムゾーン表示 APIレスポンスへの対応 まとめ 多言語対応とタイムゾーン対応 今回の国際化対応のため、主に行ったのは以下の2点です。 多言語対応 ユーザーが利用する言語に応じて、アプリケーションの表示言語を切り替える機能です。国際化対応の中核を成す要素の一つであり、異なる母語のユーザーに対して、より親しみやすいサービスを提供することが目的です。 タイムゾーン対応 異なる地域のユーザーが同じアプリケーションを利用する際に、それぞれの地域の時間に合わせて正確な時刻情報を提供する機能です。異なるタイムゾーンを考慮することで、ユーザーは常に正確な時刻情報を得ることができます。 それぞれについて対応したことや課題について書いていきます。 多言語対応 辞書の作成 多言語対応を実現するためには、各言語に対応した辞書を作成する必要があります。 辞書には言語ごとにキーと対応するテキストが設定されています。 例えば、英語の場合は「こんにちは」に対して「Hello」、また「おはよう」に対して「Good morning」というように対応付けられます。 // ja.json { "こんにちは" : "こんにちは" , "おはよう" : "おはよう" } // en.json { "こんにちは" : "Hello" , "おはよう" : "Good mornig" } 上記のように日本語がキーとなる構成の辞書を作成しました。 この辞書のメリット・デメリットは以下です。 メリット キー名を考える必要がなくなる よく見る辞書の構成は以下のように、英語がキーとなる構成です。 「こんにちは」「おはよう」であれば単純ですがもっと長いテキストの場合はキー名を考えるのがそもそも大変になります。 その点日本語がキーであれば、キー名を考える手間が省けます。 // ja.json { "HELLO" : "こんにちは" , "GOOD_MORNING" : "おはよう" } // en.json { "HELLO" : "Hello" , "GOOD_MORNING" : "Good mornig" } テンプレート上で辞書の探索をしなくても表示される文言がわかる 例えば、以下のようにキーが英語の場合、実際にアプリ上に表示されている文言が一目ではわかりません。表示されている文言を知るには辞書を調べる必要があります。 対して日本語の場合は一目でどんな文言が表示されているかがわかる為、辞書を調べる手間が省けます。 // キーが英語の場合 < div > {{ 'APP_SETTING' | translate }} </ div > // キーが日本語の場合 < div > {{ 'アプリケーションの設定' | translate }} </ div > デメリット 文言を修正する際にキーとセットで変更する必要がある 日本語がキーの辞書の場合、「 アプリケーションの設定 」というテキストを「 アプリケーション設定 」に変更したい場合、キーとテキストの両方を変更する必要があり手間が増えます。 対して英語がキーの場合は、テキストの意味合いが大きく変わらない場合はキーを変更する必要がありません。 // ja.json // キーが日本語の場合 { 'アプリケーションの設定' : 'アプリケーションの設定' } ↓ { // キーの値を変更する必要がある 'アプリケーション設定' : 'アプリケーション設定' } // キーが英語の場合 { 'APP_SETTING' : 'アプリケーションの設定' } ↓ { // キーの変更は必要でなない 'APP_SETTING' : 'アプリケーション設定' } 注意点 この構成の辞書では以下のような場合に注意が必要です。 「月」という日本語を英語にした際に「Monday」と訳すか「Moon」と訳すかは場合によって異なります。 この場合は以下のようにサフィックスをつけるようにして翻訳後の値を明確にすること必要です。 // ja.json { "月_monday" : "月" , "月_moon" : "月" } // en.json { "月_monday" : "monday" , "月_moon" : "moon" } どんな辞書の構成にもメリット・デメリットはあると思うので、色々な構成を検討することが大切です。アプリケーションにマッチする辞書構成を考えてみてください。 翻訳サービスの実装 実装 アプリケーションの言語を切り替えるには、翻訳サービスが必要です。 このサービスではアプリケーション初期化時 or 言語を変更した際にユーザーが選択した言語で辞書をロードし、辞書から対応するテキストを取得します。 実装は以下です。 @Injectable ({ providerIn : 'root' }) export class TranslateService { // アプリケーション初期化時 or 言語変更時にinitalizeを呼ぶ initialize (){ // 辞書をロードし、ロードした辞書を保持しておく } getValue ( key: string ) { // ロードした辞書からkeyで探索した値を返す } } componentではTranslateSeriviceをDI( DependecyInjection )してTransetService#getValueで翻訳を行います。 // component this .translateService . getValue ( 'こんにちは' ) テンプレート上ではpipeを介して、TranslateService#getValueで翻訳を行います。 translatePipeの中でTranslateService#getValueを呼びます。 // html < div > {{ "こんにちは" | translate }} </ div > // translate.pipe.ts @Pipe ({ name : 'translate' }) export TranslatePipe implements PipeTransform { constructor ( private translateService: TranslateService ){} transform ( key: string ) : string { return translateService . getValue ( key ) ; } } 特殊な翻訳パターンへの対応 翻訳といっても単純にテキストを翻訳するだけではありません。 翻訳時に考えるべき特別なパターンについて説明します。 翻訳するテキストに変数が入る場合 以下のようにユーザーが任意の値にできるデータ名が入った文言を翻訳する場合です。 「dataName」を削除しますか? こちらはtranslateService#getValueの引数に変数に当たる値を渡すことで実現しました。 // component this .translateService . getValue ( '「dataName」を削除しますか?' , { dataName : 'テスト' }) // html < div > {{ '「dataName」を削除しますか?' | translate: { dataName: 'テスト' } }} </ div > // → 「テスト」を削除しますか? 辞書のvalueでは変数部分を${{}}で囲むことで、明示的にこの値が変数であることがわかるようにします。 getValue関数で${{}}で囲まれた値を探索して渡ってきた値を挿入するようにすることで実現しました。 辞書は以下のような形になります。 // ja.json { "${{dataName}}を削除しますか?": "${{dataName}}を削除しますか?" } // en.json { "${{dataName}}を削除しますか?": "Do you want to delete ${{dataName}}?" } 複数形対応 主に単位を翻訳する場合に複数形対応が必要になります。 例えば日本語でカメラの数を表示する場合は1台、2台と表記します。これを英語表記にすると1device, 2devicesとなります。 複数形対応が必要な場合はTranslateService#getValueにoptionとしてアイテムのカウントを渡すようにして実現しました。 テンプレートでは以下のようにして利用します。 // html // itemCountから複数形表示にするかどうかを判断する < div > {{ deviceCount }}{{ '台' | translate: { itemCount: deviceCount } }} </ div > 辞書のvalueは、オブジェクトとして複数形と単数形の両方の値を持つようにしました。 // ja.json { "台" : "台" , } // en.json { "台" : { "singular" : "device" , "plural" : "devices" } , } TranslateServiceはitemCountから複数形表示にするかどうかを判断します。 複数形は言語毎にルールが異なるのでそれぞれ対応が必要になります。 ※今回は独自にロジックを実装しましたが、 Intl.PluralRules を上手く使えばそれぞれの言語のルールを気にする必要がなくなりそうなので移行していきたい Serviceで実装することの問題点 翻訳の仕組みをサービスで実装することでいくつかの問題点がありました。 一番の問題はServiceとして実装することで、 DIしなければならない ことです。 以下で問題点について説明します。 Angular外では利用できない 例えばServiceWorkerなどでプッシュ通知を実装しており、ServiceWorker内でプッシュ通知のテキストを定義している場合などです。 ServiceWorkerはAngular外の話なので、サービスを利用することができず翻訳できないという問題があります。 野良関数ではDIができない 例えば以下のようにtypeによって表示するテキストを返す関数の場合です。 export function typeToString ( type: string ) { if ( type === 'hoge' ) { return 'こんにちは' } else if ( type === 'huga' ) { return 'おはよう' } } このような関数の場合は、使う側でTranslateServcieをDIして、この関数にTranslateServiceのインスタンスを渡せば翻訳自体は可能です。 しかし、テキストを返す関数を実装する場合は、以下のようにtranslateServiceを引数に渡せるように実装しなければならなくなります。 // 利用する側 const text = typeToString ( 'hoge' , this. translateService ) ; クラスの外だとDIできない 以下のように定数として定義しており、オブジェクトのテキストをキーに応じて表示する場合などです。 この場合はDIができない為、このままでは翻訳が難しいです。 // component const TEXT_MAP = { 'huga' : 'ふが' , 'hoge' : 'ほげ' } ※関数や定数に関しては、戻り値を翻訳する方法もありますが、使用する側で翻訳処理を挟まなければならないことや翻訳の漏れを考慮すると、文言を定義した場所で翻訳する方が良いと考えています。 // これだと使う側で必ず翻訳をしなければならない // typeToStringの中だけを見た時に翻訳されているかどうかはわからない const text = typeToString ( 'hoge' ) const translatedText = translateService . getValue ( text ) ; 解決方法 詳細は省きますが、TranslateServiceとほとんど中身は同じtranslate関数という関数をjsで実装しました。(DIせずに基本的にはどこからでも呼び出せる。) jsで実装することでServiceWorker内でも使用できるようになります。 translate関数を実装したことで上記の課題は解決できました。 タイムゾーン対応 タイムゾーンの取得 ユーザーが居住している地域のタイムゾーンを正確に取得することは、タイムゾーン対応の実現に重要です。 通常、ブラウザやデバイスから提供される情報を利用して、ユーザーの現在のタイムゾーンを特定します。 ブラウザのタイムゾーンIDを取得するには、 Intl.DateTimeFormat().resolvedOptions().timeZone を使用します。 このプロパティはタイムゾーンIDが取得できない場合に Etc/Unknown という値を返す可能性があるため、この値が返ってくる場合の考慮も必要になります。 タイムゾーン表示 アプリケーション内で時刻情報を表示する際には、ユーザーのタイムゾーンに合わせて適切な時刻を表示する必要があります。これにより、ユーザーは自分の地域に合った時刻を確認することができます。 国際化対応において Unixtime で時間を扱うことはとても重要になってきます。 例えば以下のようにユーザーが2024年4月29日午後1時の映像を再生することを考えます。この日付はユーザーが異なるタイムゾーンに住んでいる場合、表示される時刻が異なります。 SafieViewer 具体的には以下のように時刻が異なるため表示を変更する必要があります。 東京 (日本標準時、JST) の場合: 2024年4月29日午後1時 ベトナム (ベトナム標準時, ICT)の場合: 2024年4月29日午前11時 ニューヨーク(東部標準時、EST)の場合: 2024年4月29日午前12時 UnixTimeで時間を扱っていれば、JavaScriptの Date オブジェクトがブラウザのタイムゾーンに合わせて適切に表示を変更してくれます。 APIレスポンスへの対応 Unixtimeで時間を扱っている場合は問題ないですが、既存のAPIがJST(日本標準時)のレスポンスを持つ場合があります。 このような場合、API側に表示すべきタイムゾーンを知らせるため、リクエストにtzパラメータとして取得したタイムゾーンIDを渡し、API側でそれに応じたレスポンスを返すように修正する等の対応が必要になります。(今回はこの対応を行いました) 新規にAPIを実装する場合は、時間関連のレスポンスをUnixTimeで扱い、フロントエンド側でタイムゾーンIDに応じて時刻表示を切り替えることでより実装が効率的になると思います。 まとめ 今回のブログでは、簡単に多言語対応とタイムゾーン対応についてそれぞれの実装方法や課題について解説しました。 国際化対応では言語や地域の違いを考慮し、ユーザーにとって使いやすいアプリケーションを提供することが重要です。 今回の取り組みを通じて、国際化対応の重要性や実装方法について深く理解することができました。 今回の内容を基に、さらなる機能改善や拡張を行い、より多くのユーザーに価値を提供していきたいと考えています。 セーフィーではエンジニアを積極的に採用しています。 興味がある方は是非下記サイトを一度覗いてみてください。 https://safie.co.jp/teams/engineering/
自己紹介と本日のテーマ 伝えたいこと 発生した問題 観点出しを行ったことによる効果 まとめ 自己紹介と本日のテーマ こんにちは、セーフィーで品質保証業務に従事しているQAエンジニアの佐藤です。 本日は「テストケース作成前の観点出しの重要性」をテーマにお話しします。 このテーマを選んだ理由は、最近のQA業務で実際に起きた出来事から、観点出しの重要性を改めて実感したためです。 今回のブログでは、観点出しをおろそかにした結果発生した問題と、その後の改善策について、自戒の意味も含め共有しようと思います。 伝えたいこと ソフトウェア開発において、品質保証の一環としてテストの実施が不可欠です。そしてそのためのテストケースの作成が必要となります。 しかし、軽微な修正や工数不足などで見落とされがちなのが、テストケース作成前の「観点出し」です。 この記事では、観点出しをおろそかにした結果発生した問題と、その後の改善策について共有します。 発生した問題 僕の担当していたプロジェクトは少数の開発メンバーで限られたリソースの中で開発を行っていました。 最近は開発メンバーや企画メンバーが増え、AIを利用したアプリをリリースするなど、僕が入社した2022年よりも高度な技術や、広い知見が求められるサービスおよびアプリが増えているように感じています。 以前はリリースごとの追加機能のボリュームが少なく、また携わるメンバーも少なかったため、観点の抽出はメンバー同士のコミュニケーションで完結でき、形式的な観点出しのアウトプット資料などを作ることなく進めてしまっていました。 また、自分自身観点出しの重要性がわかっておらず、そのままテストケースを作成したほうが早いのではないかという思いもあったことから、あまり熱心に観点出しを実施してきませんでした。 そのような状況の中で初めて自分が主体となってテスト設計・実施を行う場面がやってきたのですが、以下のような事が発生してしまいました。 実施すべきテストが実施されずにソフトウェア品質の低下を招いた テストケースの体裁や手順や期待値などのレギュレーションにも一貫性がなく他のメンバーが見たときに混乱を招いた 上記のことから結果的にテストケースを一から作り直さなければならず、より作業工数が増大してしまうという大変な思いをしました。 観点出しを行ったことによる効果 些細な改修内容でもテストケースの作り直し等の問題を避けるため、テストケースを作成前には必ず観点出しを行うように心がけました。基本的なことですが非常に重要な作業で、具体的には以下の流れで作業を実施しました。 QAチーム内での観点のレビュー: 該当のプロダクトのQAに携わるメンバーにレビュー依頼を行い、機能ごとにテストすべきポイントを洗い出す ポイントの洗い出しには各機能がどのような要件を満たすべきか、どのように動作するのが期待値なのかを要件定義書などをしっかりと確認することや、それでも不明な点は事前に開発チームへのヒアリングを実施、エラーや例外時の状況でどのように動作するかなどを念頭に入れて行いました。 開発・企画メンバーとの観点レビュー会の実施:QAチームのレビューを基に作成した観点表を開発・企画メンバーに共有し、テスト範囲や内容についての合意を得る ここでは実際に作成した観点の共有をする上で文字だけではくパターン表を用いた見やすい資料作りを心がけたり、追加で確認したい事項などを事前にまとめておきレビュー会当日の限られた時間の中でスムーズに話すことができるように工夫をしました。 以上のことを踏まえて今回テストケースの作成前に観点出しを行った結果、以下のような効果がありました。 テスト観点の可視化を行うことでの開発、QAメンバーとの意思疎通のコストの削減 テスト観点の可視化により、何のためのテストか誰が見てもわかりやすくなり、QAメンバーからテストケースの手順や、期待値についての質問を受けることが少なくなりました。 また、複数のプロダクトのQAが重なっている場合では特に負担となるのはQA期間中の開発チームへの仕様確認です。 あとから追加で仕様確認することはある程度は仕方ないと思ってはいますが、観点漏れを少なくすることでその仕様確認する件数を減らすことができます。 その結果、全体の工数も削減することができました。 テスト観点の網羅性が向上したことでのテスト自体の品質の向上 テスト実施対象の粒度の均一性や、テスト項目作成にあたっての体裁の一貫性の向上 観点出しを行うことで、テストケースの抜け漏れが大幅に減少しました。 あらかじめ定めた観点に沿ってテストケースを作成することで、テストケースの組み立て方を整理しながら作成することができ、誰が見ても理解しやすいテストケースにすることができたと感じています。 まとめ これらの経験を通じて、テストケースを作成する前に「観点出し」を行うことの重要性を知ることが出来ました。 時間が限られている中でも観点だしを省略せず行うことで、結果的に効率的かつ効果的なテストケースを作成することができるので、どのプロダクトにおいても観点出しを作業のフローに取り入れ、品質向上を目指すことが重要だと感じました。 最後に、セーフィーではより良いプロダクトを作り上げ、成長していく仲間を探しています。 この記事を読んで共感する点がありましたら、ぜひ採用ページもご覧ください。 ※採用情報の詳細は こちら からご覧いただけます。 最後まで読んでいただきありがとうございました。
こんにちは!Safie第2開発部のiOS Engineerアダム( @monolithic_adam )です!今年の夏暑いですね! でも夏といえばあれしかないでしょう!そうiOSDC Japanです! 今年もみんな集まって早稲田でワイワイするiOSイベント! 最高の企画してくれたので #iwillblog 熱が冷めないうちにブログ書いています! Day 0は残念ながら現地参加できなかったけどオンラインで楽しくみていました! 各トラックを切り替えながら楽にトーク見えてハイブリッド頑張ってくれているiOSDCに感謝! コードバトルは気になっていて、見始めたら面白くてほぼDay 0全部コードバトル見てしまったw Award 🥇 Favorite Talks Favorite Booth Favorite Activity Favorite Novelty Closing Award 🥇 今年は完全に参加者枠でゆっくり色々楽しめました。楽しかった・よかった企画・ノベルティ紹介したいです! Favorite Talks 一つに絞れなかったので何個か選びました! fortee.jp 最近エラーハンドリングの設計悩んでいるところあるのでこちら本当に助かりました! typed throw使いまくってもいいと思ったら、koherさんのトークでそれを考え直せるまでの素晴らしいトークでした。 fortee.jp Strict Concurrencyの一歩踏み始まっているからこちらもちょうどよかった内容の一つでした。苦労している部分は確かに 現状の超えてはいけないlayerあるな〜と思って資料もう一回見返して、作戦考える! fortee.jp 脆弱性はみんななんとなく把握していると思いますが、こちら本当によかったです!よくある実装・わかりやすい例がいっぱいあって (資料英語だったのもとてもわかりやすかったし😉)モバイルエンジニアみんな一回見るべきセキュリティトークです! fortee.jp GBAとSwiftはゲーマーとして見るしかないと思ってとても面白かったです!こちらをInspirationで、最近N64のコンパイルの Breakthroughで似ていることできるのかな〜と考え始めてめっちゃよかったトークです! fortee.jp MetricKitは使ったことないので、めっちゃ気になったトーク、その何ms以上じゃないとペイロード入らないとか はとても役に立つ情報でした! Favorite Booth うちわでピッタリ1.8m/sできたら賞品もらえる企画本当に楽しかったです! 来年の企画楽しみにしています! Favorite Activity 夏祭っぽいフェースペイント・ネイルで楽しく遊びました!フェースペイントは会社のロゴカスタムで頼めて最高でした! PRでTwitter置いておきます! #iosdc ペイントは無事に終了致しました。3日間に渡り沢山の方々に描かせて頂きました。お客様の持参される絵柄を一発勝負で描くのはとても刺激的でした。いつもお子様対象なので大人が楽しんでくれたのも良きでした!ありがとうございました✨ #iosdc2024 #iosdcpaint #フェイスペインターミホウ pic.twitter.com/0gaaZmpGqt — フェイスペインター☆ミホウ (@fp_mihoo) 2024年8月24日 Safieのロゴをフェースペイントブースで描いてもらえた〜 1階アートブースにはネイルコーナーがあります。 ネイルをしたら是非 #iosdcnail をつけて投稿してくださいね! #iosdc pic.twitter.com/ZAnFsRBSap — iOSDC Japan (@iosdcjp) August 23, 2024 初ネイルで目覚めたかも! Favorite Novelty iOSDCロゴ入り扇子 暑と戦う夏、扇子が大事な道具になるのでiOSDCロゴ入り扇子は嬉しかったです! SPIDERPLUSさんのAnker充電器 Anker mini小さくて便利!出張先で役に立つ! 食べログさんの靴下 黄色くて可愛いソックスでサンダルじゃない時にはく!w メルカリさんのチェキ ちょっと懐かしいPolaroid写真取れて楽しかったです!ネームタグに入れて可愛さアップできたし! iOSDCネイルシール iOSDCで初ネイル楽しかった上に、娘に大好評!次の週末出かける時につける約束している! Closing 2017から参加してきたiOSDCは今年も本当に大成功でした!来年の開催をもう楽しみにしています! Safieのブースで会いましょう!😉 iOSDC今年まじで最高でした!コンテンツも企画も全部良すぎて明日からiOSDC ロスしかない!💅🎨🍻 また来年会いましょう!!! #iosdc pic.twitter.com/yDVZ8oHApo — Adam Henry (@monolithic_adam) August 24, 2024 モバイルチームは開発する仲間を募集しています! 一緒にiOSDC参加しましょう! open.talentio.com
はじめに セーフィー株式会社 で画像認識AIの開発エンジニアをしている今野です。 今回は、最新の物体検出アルゴリズムであるYOLOv8を活用して、特定エリアを通過する車両を自動的にカウントするシステムの実装方法をご紹介します。このシステムは、交通量調査や駐車場の利用状況分析など、様々な場面で応用可能な技術です。 本記事では、YOLOv8による物体検出から、検出結果の後処理、そして実際の車両カウントまでの一連のプロセスを、具体的なコード例を交えて解説していきます。AIを活用した実用的なソリューションの構築に興味のある方々にとって、有益な情報となれば幸いです。 はじめに やりたいこと 実施手順 環境構築 YOLOv8 による物体検出と追跡 コマンドの各引数の説明 保存されるファイル 検出結果の後処理 車両カウントの実施 スクリプトの使用方法 課題 まとめ 最後に やりたいこと 本プロジェクトでは、以下の機能を実現することを目指します: カメラ映像を使った通行量カウント:道路を通過する車両を検出し、カウントします。 複数種類の対象物の識別:自動車、トラック、バス、自転車など、異なる種類の通行物を区別してカウントします。 特定エリアでのカウント:映像内の特定の範囲(例:交差点や横断歩道)を通過する対象物だけをカウントします。 実施手順 YOLOv8を使用した通行量カウントシステムの実装は、以下の手順で進めていきます: 環境構築 YOLOv8による物体検出と追跡 検出結果の後処理 車両カウントの実装 結果の出力 これらの手順を通じて、YOLOv8を使用した基本的な通行量カウントシステムを構築していきます。各ステップの詳細は、以降のセクションで具体的に解説していきます。 環境構築 まず、必要なライブラリをインストールします。以下のコマンドを実行してください。 pip install ultralytics opencv-python numpy matplotlib shapely YOLOv8 による物体検出と追跡 環境構築後、コマンドラインから検出・追尾を実行します yolo task=detect mode=track model=yolov8x source=/*対象の動画*/ save_txt save_conf save=True project=/*保存先ディレクトリ*/ classes=2,3,4,6,8 上記のコマンドを実行すると、指定した動画ファイルに対して物体検出と追跡が行われ、結果が指定したディレクトリに保存されます。 コマンドの各引数の説明 task : 実行するタスクを指定します。ここでは物体検出と追跡を行うために track を指定しています。 detect を指定すると、物体検出のみが実行されます。 model : 使用するYOLOv8モデルを指定します。 yolov8x は、YOLOv8の大きいサイズのモデルを指します。必要に応じて、 yolov8s や yolov8l など他のサイズのモデルを指定することも可能です。 source : 推論を行うソースを指定します。 save_txt : このオプションを指定すると、検出および追跡結果がテキストファイルとして保存されます。各フレームごとにオブジェクトのクラスID、バウンディングボックスの座標などが記録されます。 save_conf : このオプションを指定すると、各検出結果の信頼度スコアもテキストファイルに保存されます。これにより、どの程度の信頼度で物体が検出されたかを確認できます。 save : Trueにすることで動画ファイルが指定したディレクトリに保存されます。この後の車両カウントに動画ファイルは不要なので、saveはFalseにして処理を高速化することも可能です。 project : 結果を保存するルートディレクトリを指定します。ここで指定されたディレクトリの中に、結果が保存されます。指定がない場合、デフォルトで runs ディレクトリに保存されます。 lasses : このオプションは、検出するクラスを特定のIDに絞るために使用します。指定したクラスIDに対応する物体のみが検出されます。今回使用するモデルはCOCO datasetのカテゴリidに準拠しているため、”2,3,4,6,8”を指定すると自転車、自動車、オートバイ、バス、トラックのみが検出されるようになります。 保存されるファイル 動画ファイル : 検出結果が入力動画に対して重畳されたビデオファイルが保存されます。 project オプションで指定したディレクトリの中に、検出結果が保存されたフォルダが作成され、その中に動画ファイルが保存されます。 テキストファイル : 検出された各フレームのオブジェクト情報が保存されたテキストファイルが生成されます。各フレームに対応するテキストファイルが保存され、クラスIDやバウンディングボックスの座標などの情報が含まれています。 出力テキストファイル例: 7 0.467559 0.775986 0.196506 0.249537 0.94994 1 7 0.302576 0.186449 0.136675 0.144221 0.91664 2 7 0.70413 0.417058 0.118785 0.119517 0.897981 3 7 0.717557 0.0861716 0.0540201 0.106011 0.838547 4 2 0.0168792 0.141835 0.0337098 0.0552144 0.810139 5 2 0.0770008 0.229309 0.068876 0.0676976 0.728882 6 2 0.532953 0.29052 0.0817046 0.088725 0.653665 7 2 0.194599 0.215173 0.0581096 0.0713775 0.633438 8 2 0.322347 0.272015 0.0641675 0.0660537 0.620266 9 検出結果の後処理 次に、YOLOv8で得られたトラッキング結果を車両カウントで使いやすい形へ変換する処理を行います import argparse import glob import os import re import cv2 import matplotlib.pyplot as plt import numpy as np import pandas as pd from tqdm import tqdm from ultralytics.utils import yaml_load from ultralytics.utils.checks import check_yaml CLASSES = yaml_load(check_yaml( "coco128.yaml" ))[ "names" ] def parse_args () -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "--input_video" , type = str , required= True , help = "Path to the input video file" ) parser.add_argument( "--input_dir" , type = str , required= True , help = "Directory containing the input label files" ) parser.add_argument( "--output_dir" , type = str , default= "out" , help = "Directory to save the output files" ) parser.add_argument( "--output_labels" , type = str , default= "output_video_results.csv" , help = "Name of the output CSV file" ) parser.add_argument( "--vid_stride" , type = int , default= 1 , help = "Video stride for processing" ) args = parser.parse_args() return args def get_video_resolution (video_path: str ) -> tuple [ int , int ]: """指定された動画の解像度を取得する Args: video_path (str): 動画のファイルパス Returns: tuple[int, int]: 解像度(width, height) """ cap = cv2.VideoCapture(video_path) try : width = int (cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int (cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) except Exception as e: print (f "An error occurred: {e}" ) finally : cap.release() return width, height def get_frame_num_from_label_file (label_file: str ) -> int : """001.txtのような入力から0埋めなしの数字を取得する Args: label_file (str): 先頭に番号が付いたファイル名 Returns: int: 番号 """ match = re.search( r"(\d+)\.txt" , label_file) assert match is not None return int (match.group( 1 )) def merge_labels (input_dir: str , input_video_path: str , output_dir: str , output_labels: str ) -> None : """txtファイルを結合し、csvファイルを出力する Args: input_dir (str): 入力txtファイルが格納されているディレクトリ input_video_path (str): 入力動画のファイルパス output_dir (str): 出力ディレクトリ output_labels (str): 出力csvファイル名 """ # labelsファイルの一覧を取得 label_files = glob.glob(os.path.join(input_dir, "*.txt" )) # ファイル名のフレーム番号部分(0埋めなしの数字)で昇順ソート label_files.sort(key= lambda x: get_frame_num_from_label_file(x)) # 入力動画から解像度情報を取得 width, height = get_video_resolution(input_video_path) # txtファイルを読み込み、csvファイルを出力 df_list = [] column_names = [ "class_id" , "center_x" , "center_y" , "width" , "height" , "confidence" , "tracking_id" ] for label_file in tqdm(label_files, desc= "Loading labels" ): # ファイル名からフレーム番号を取得 frame_num = get_frame_num_from_label_file(label_file) # ファイルを読み込み df = pd.read_csv(label_file, sep= " " , header= None ) # カラム名を設定 df.columns = column_names # 正規化座標をピクセル座標に変換 df[ "center_x" ] = (df[ "center_x" ] * width).astype( int ) df[ "center_y" ] = (df[ "center_y" ] * height).astype( int ) df[ "width" ] = (df[ "width" ] * width).astype( int ) df[ "height" ] = (df[ "height" ] * height).astype( int ) # フレーム番号を追加 df[ "frame_num" ] = frame_num # クラスラベルを追加 df[ "class_label" ] = df[ "class_id" ].apply( lambda x: CLASSES[x]) # カラムを並べ替え df = df[ [ "frame_num" , "class_label" , "class_id" , "center_x" , "center_y" , "width" , "height" , "confidence" , "tracking_id" ] ] df_list.append(df) # データフレームを出力 concat_df = pd.concat(df_list) concat_df.to_csv(os.path.join(output_dir, output_labels), index= False ) def aggregate_labels (output_dir: str , output_labels: str ) -> None : """ラベルを集約する Args: output_dir (str): 出力ディレクトリ output_labels (str): 出力csvファイル名 """ # 結合したラベルファイルを読み込み concat_df = pd.read_csv(os.path.join(output_dir, output_labels)) # tracking_idごとにclass_idの出現頻度をカウント class_id_counts: dict [ int , dict [ int , int ]] = {} for _, row in concat_df.iterrows(): if row[ "tracking_id" ] not in class_id_counts: class_id_counts[row[ "tracking_id" ]] = {} if row[ "class_id" ] not in class_id_counts[row[ "tracking_id" ]]: class_id_counts[row[ "tracking_id" ]][row[ "class_id" ]] = 0 class_id_counts[row[ "tracking_id" ]][row[ "class_id" ]] += 1 # tracking_idごとにclass_idの多数派を算出 class_id_map: dict [ int , int ] = {} for track_id, class_id_count in class_id_counts.items(): class_id_map[track_id] = max (class_id_count, key=class_id_count.__getitem__) # 多数派のクラスで上書き for track_id, class_id in class_id_map.items(): concat_df.loc[concat_df[ "tracking_id" ] == track_id, "class_id" ] = class_id concat_df.loc[concat_df[ "tracking_id" ] == track_id, "class_label" ] = CLASSES[class_id] concat_df.to_csv(os.path.join(output_dir, output_labels), index= False ) def main (): args = parse_args() # ディレクトリの存在確認 if not os.path.exists(args.input_dir): print ( "input_dir not found" ) return if not os.path.exists(args.output_dir): os.makedirs(args.output_dir, exist_ok= True ) # ファイルの存在確認 if not os.path.exists(args.input_video): print ( "input_video not found" ) return # ラベルファイルを結合 merge_labels( input_dir=args.input_dir, input_video_path=args.input_video, output_dir=args.output_dir, output_labels=args.output_labels, ) # ラベル集約 aggregate_labels(args.output_dir, args.output_labels) if __name__ == "__main__" : main() このスクリプトは、YOLOv8が生成したラベルファイルを読み込み、各トラッキングIDごとに検出されたクラス情報を集約して、最も頻繁に出現したクラスをそのトラッキングIDに関連付けます。これにより、車両の種類を安定して識別し、車両カウントに適したデータ形式に変換します。 スクリプトの主な機能 YOLOv8の推論結果から生成されたラベルファイルを一つのCSVファイルに結合 正規化された座標(0~1の範囲)をピクセル単位の座標に変換 各トラッキングIDに対して、最も頻繁に出現したクラスをそのトラッキングIDの最終クラスとして指定する すべての情報を一つのCSVファイルにまとめ、車両カウントに適した形式で保存する 上記のスクリプトをPythonファイルとして保存し、コマンドラインから実行します。 出力CSVファイル例: 車両カウントの実施 次に車両カウントを実施します import pandas as pd from shapely.geometry import Point, Polygon import argparse def load_data (csv_file: str ) -> pd.DataFrame: """CSVファイルを読み込み、データフレームを返す Args: csv_file (str): CSVファイルのパス Returns: pd.DataFrame: 読み込んだデータを格納したPandasデータフレーム """ return pd.read_csv(csv_file) def define_area (points: list [ tuple [ float , float ]]) -> Polygon: """エリアを構成する座標リストを受け取り、Polygonオブジェクトを返す Args: points (list of tuples): エリアを定義する座標のリスト [(x1, y1), (x2, y2), ...] Returns: Polygon: ShapelyのPolygonオブジェクト """ return Polygon(points) def count_objects_in_area (df: pd.DataFrame, area_polygon: Polygon) -> dict : """データフレームとエリアのポリゴンを受け取り、エリア内のオブジェクトをクラスごとにカウントする Args: df (pd.DataFrame): オブジェクトのデータを含むデータフレーム area_polygon (Polygon): 対象エリアを定義するPolygonオブジェクト Returns: dict: 各クラスごとのオブジェクト数をtracking_idで集約した辞書 """ counts = {} for _, row in df.iterrows(): class_label = row[ 'class_label' ] tracking_id = row[ 'tracking_id' ] center_x = row[ 'center_x' ] center_y = row[ 'center_y' ] # オブジェクトの中心点がエリア内にあるかを確認 point = Point(center_x, center_y) if area_polygon.contains(point): if class_label not in counts: counts[class_label] = set () counts[class_label].add(tracking_id) return counts def print_counts (counts: dict ) -> None : """カウント結果を出力する Args: counts (dict): 各クラスのオブジェクト数を保持する辞書 """ for class_label, tracking_ids in counts.items(): print (f "{class_label}: {len(tracking_ids)} objects" ) def parse_args () -> tuple [ str , list [ tuple [ float , float ]]]: """コマンドライン引数を解析する Returns: tuple: CSVファイルのパス (str) とエリアの座標リスト (list of tuples) を含むタプル """ parser = argparse.ArgumentParser(description= "Count objects in a specified area from a CSV file." ) parser.add_argument( "csv_file" , type = str , help = "Path to the CSV file containing the object data." ) parser.add_argument( "area_points" , type = float , nargs= '+' , help = "List of coordinates defining the area (x1 y1 x2 y2 ...)." ) args = parser.parse_args() # エリアの座標をペアに分割してリストに変換 if len (args.area_points) % 2 != 0 : raise ValueError ( "The number of coordinates for area_points must be even." ) area_points = [(args.area_points[i], args.area_points[i+ 1 ]) for i in range ( 0 , len (args.area_points), 2 )] return args.csv_file, area_points def main () -> None : # コマンドライン引数を解析 csv_file, area_points = parse_args() df = load_data(csv_file) area_polygon = define_area(area_points) # オブジェクトのカウントを実行 counts = count_objects_in_area(df, area_polygon) # 結果を出力 print_counts(counts) if __name__ == "__main__" : main() このスクリプトでは指定したエリアを通過するオブジェクトを各クラスごとにカウントし表示します スクリプトの使用方法 コマンドラインでスクリプトを実行し、CSVファイルのパスとエリアの座標を指定します。 CSVファイルは先ほど検出・追跡結果を変換し作成したファイルを指定し、エリアの座標はエリアを構成する3点以上の多角形の各頂点のx座標とy座標のペアで指定します。 座標の確認方法は動画から切り出した画像を使用してペイント等で取得することができますが、 こちら のような外部のサイトを使用して取得することも可能です 次の例では、 data.csv というファイルを使用し、エリアを構成する4つの座標 (400, 400), (600, 400), (400, 600), (600, 600) を指定しています。 python count_object.py output_video_results.csv 400 400 600 400 400 600 600 600 スクリプトを実行することで、指定したエリア内にある各クラスのオブジェクト数を確認することができます。 car: 23 objects truck: 6 objects 課題 クラス誤検知 現在のシステムでは、ミニバンを正確に分類できず、しばしばバスやトラックとして誤認識する問題が発生しています。この原因として、YOLOv8で使用しているモデルがCOCO Datasetを基に学習されていることが挙げられます。COCO Datasetは主に海外の画像を使用しており、海外ではミニバンの普及率が低いため、モデルがミニバンを十分に学習していない可能性があります。 この問題を解決するためには、国内のデータを収集し、それを用いてモデルを追加学習(ファインチューニング)することが考えられます。これにより、ミニバンの認識精度を向上させ、誤認識のリスクを減らすことが期待できます。 まとめ 今回の記事では、YOLOv8を用いた通行量カウントシステムの構築手順を詳細に解説しました。このシステムは、YOLOv8による高精度な物体検出機能を活用し、道路や駐車場といった特定エリア内を通過する車両を自動的にカウントするものです。 本実装は、基本的な車両カウントシステムですが、機能を拡張することでさまざまな用途に応用可能です。以下に、いくつかの実装アイデアを紹介します 多様なオブジェクト検出 機能:車両以外のオブジェクトも検出可能 特徴:COCOデータセットの80種類のクラスに対応 実装:クラス指定の変更で簡単に実現 応用例:歩行者、自転車、野生動物の調査 複数エリア間の移動追跡 機能:特定エリア間の移動オブジェクトをカウント 特徴:複数ポリゴンエリアでオブジェクトの軌跡を追跡 実装:エリア定義と軌跡追跡ロジックの追加 応用例:交差点の右左折車両計測、店舗の入退店客数把握 リアルタイムカウント 機能:ライブ映像からのリアルタイム解析 特徴:即時的なデータ取得と分析が可能 実装:入力ソースを録画動画からライブカメラ映像へ変更 応用例:交通量モニタリング、イベント会場の人流分析 最後に セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います 最後までお読みいただき、ありがとうございました。
マイページ開発の背景 ユーザー情報の閲覧、変更がSafieViewerの1ページとして存在していた セッション管理はアプリケーション間で共通であるにもかかわらず、ログイン画面が各アプリケーションに存在し、個別にメンテナンスされていた アプリケーションのラインナップが増えていく中で、各アプリケーションの相互遷移ができない 課題に対するアプローチ マイページの開発 利用中のアプリケーション一覧の表示 ユーザー情報、デバイス・契約情報、明細の閲覧 ログイン機能をマイページに統合する 共通ヘッダーの開発 ログインセッションの管理 終わりに こんにちは、フロントエンドエンジニアの大場です。 本記事では、2023年2月にリリースしたユーザー情報を管理するアプリケーション(以下、マイページ)とログイン機能の統合について、開発の背景および経緯を交えてご紹介します。 マイページ開発の背景 マイページ開発以前は以下のような課題がありました。 ユーザー情報の閲覧、変更機能が映像視聴アプリケーション(以下、SafieViewer)の1コンテンツとして存在していた。 ログインのセッション管理はアプリケーション間で共通であるにもかかわらず、ログイン画面が各アプリケーションに実装され、メンテナンスも個別に行われていた。 アプリケーションのラインナップが増えていく中で、各アプリケーションの相互遷移の導線が無く、利便性が低下していた。 アプリケーションのラインナップ(一部を抜粋) ユーザー情報の閲覧、変更がSafieViewerの1ページとして存在していた 創業当時はSafieViewerしかアプリケーションが存在しておらず、ユーザー情報や契約情報の閲覧・変更などの処理もこのアプリケーションですべて行っていました。それから数年が経過し、事業規模の拡大に合わせて多数のアプリケーションが追加されましたが、ユーザー情報や契約情報の閲覧や変更機能は依然としてSafieViewerの1コンテンツとして存在していました。そのため、他のアプリケーションがSafieViewerに機能を依存しており、またユーザー関連の一部の機能は各アプリケーション側で独自に再実装されていました。さらに、SafieViewer側の制約が大きく、機能拡張を行うことが非常に困難な状況でした。 セッション管理はアプリケーション間で共通であるにもかかわらず、ログイン画面が各アプリケーションに存在し、個別にメンテナンスされていた セッション(ログイン状態)はCookieで管理されているため、アプリケーション間で遷移した際に、利用条件を満たせば再ログインすることなく継続してアプリケーションを利用することができます。しかし、各アプリケーションが個別に新規開発および拡張がされた経緯があり、ログイン画面においても各アプリケーション側で個別に実装されていました。そのため、2段階認証やシングルサインオン(SSO)認証など、ログイン関連の機能が拡張されていく中で、各アプリケーションが足並みを揃えてそれぞれ開発を進め、リリースタイミングを合わせる、という開発上の手間が発生していた上に、品質維持のために行うQAテストのコストも膨大になっていました。 また、セッションはバックグラウンド(別タブなど)でログアウトされた際や再ログインされた際に適切に破棄する必要がありますが、これらの処理が各アプリケーション側に委ねられており、仕様に差異が生じていました。 アプリケーションのラインナップが増えていく中で、各アプリケーションの相互遷移ができない 企業の成長と共に、アプリケーションのラインナップも増えていきましたが、これらを相互遷移する仕組みがありませんでした。そのため、ユーザーがどのアプリケーションを利用可能か直感的に判断する術がなく、Web検索、あるいはブラウザのブックマーク機能などを活用してアプリケーションを遷移する必要がありました。 課題に対するアプローチ これらの課題を解決するため、以下の方針でマイページの開発を進めました。 マイページを新規開発し、ユーザー情報や契約情報の閲覧、変更機能をSafieViewerから分離し、マイページに移設する。また、各アプリケーションで個別に実装されていたログイン機能をマイページに統合し、各アプリケーションからマイページのログイン機能を統一的に利用できるようにする。 ユーザー情報や各アプリケーションのリンクを集約したヘッダーUI(以下、共通ヘッダー)を開発し、各アプリケーションで共通利用し、マイページや各アプリケーションへの導線を提供する。 マイページの開発 新規開発したマイページには主に以下の役割があります。 利用中のアプリケーション一覧の表示 ユーザーの契約情報に基づき、利用可能なアプリケーションの一覧を表示します。これにより、ユーザーがマイページを起点として各アプリケーションに容易に遷移することができるようになります。 ユーザー情報、デバイス・契約情報、明細の閲覧 これまでSafieViewerに実装されていたユーザー情報、デバイス・契約情報、明細の閲覧などの機能をマイページ側に移設しました。これにより、映像視聴以外の機能がSafieViewerから完全に切り離され、より柔軟なレイアウトの設計が可能になりました。 ログイン機能をマイページに統合する マイページ側でログインページを再実装し、各アプリケーションで共通利用できるようにしました。これにより、ログイン関連の開発をマイページ側に集約させ、各アプリケーションからはログイン周りの実装の排除を実現しました。 各アプリケーションはマイページへ遷移してログイン処理を行い、ログイン後に各アプリケーションへリダイレクトさせます。 共通ヘッダーの開発 マイページの開発により、ユーザー情報や契約情報の分離およびログイン機能の統合を実現しました。しかし、課題としてアプリケーション間の相互遷移ができない点、ログインセッションの管理が各アプリケーションに依存している点は解決できていません。そこで、この課題を解決すべく、マイページと合わせて開発したのが共通ヘッダーになります。 共通ヘッダーはマイページのヘッダー部分に配置されているUIコンポーネントであり、ユーザー情報の簡易表示やアプリケーション間の遷移機能を提供します。 共通ヘッダーは Web Components の Custom Elements としてエクスポートすることにより、各アプリケーションからはWeb標準のカスタム要素として利用することができます。Custom Elementsを採用したメリットは以下の通りです。 Web標準のカスタム要素であるため、利用するWebフレームワークに依存しない。またHTML上にカスタム要素として配置するだけで利用可能であるため、各アプリケーション側の実装がシンプルになる。 各アプリケーションと同一ドメインでスクリプトが実行されるため、ローカル開発環境などクロスドメインの環境下においても共通ヘッダー側でログインセッションを管理することができる。 マイページに実装されているリソースをそのまま流用できるため、共通ヘッダーの開発にかかるコストを最小化できる。 共通ヘッダーではユーザーが利用可能なサービス・アプリケーションが一覧として表示されます。これにより、各アプリケーションは共通ヘッダーを介して他のアプリケーションへ相互に遷移する導線を提供することができます。 ログインセッションの管理 実は共通ヘッダーはUIとして表示するだけでなく、内部でログインセッションを管理する機能も有しています。そのため、各アプリケーションは共通ヘッダーをサイト上に組み込むだけで、ログインセッションの管理を自動的に行うことができ、ログアウト時の処理を考慮する必要がなくなります。 一般的にサイト間のUIパーツ共有はiFrameが採用されることが多いですが、ログインセッションの管理にはCookieを扱うため、同一ドメインでスクリプトが実行される必要があります。その意味でも、WebComponentsを採用するメリットは大きいと言えます。 ※マイページはAngular フレームワークを採用しているため、Custom Elementsのエクスポートは Angular Elements の機能を活用しています。 終わりに 本記事ではマイページ開発の経緯と実装方針についてご紹介しました。プロダクト構成やその規模に関しては、事業規模拡大に応じてスケールしていくのが一般的です。しかし、それと同時に解決すべき課題も必ず山積します。一方で、SaaSビジネスのように絶え間なく稼働し続けることが求められ、常に進化し続けるサービスでは、課題は認識しつつも一朝一夕では解決を図ることが困難であるのも事実です。マイページの開発も足掛け丸1年ほどはかかっていますし、ログイン統合においては運用に支障が生じないよう慎重に進める必要があったため、完全な移行まで相応の期間を要しています。 しかしながら、マイページのように、大きな課題に対して1つずつ解決に向けて進めることができれば、これまで実現しなかった新機能の実装や機能拡張も可能になります。そのため、こうした負への取り組みは、企業の次の成長フェーズへステップアップさせる上で非常に重要であると言えます。本記事が多少なりとも課題解決の一助になれば幸いです。 セーフィーではエンジニアを積極的に採用しています。興味がある方は是非下記サイトを一度覗いてみてください https://safie.co.jp/teams/engineering/
Hey, it's Adam from Safie. monolithic-adam.hatenablog.com Since I joined in March I have been wanting to start doing an English event to get to know my new coworkers better. I was finally able to do it in June so I wanted to write about it! Safie English School Introduction & Icebreaker English Games Free Talk On Reflection What's Next Safie English School So because I used to be an Elementary School/English Conversation School teacher 10+ years ago I named it Safie English School. I just wanted it to be a place where me and others at Safie could use/practice/learn English in a casual setting. With other members from the Mobile Team, we limited it to members from the Tech department to see how it would do. Our Agenda went something like this: Introduction/Icebreakers English Games Free Talk The response from everyone at Safie was amazing! We had 15+ participants join and even some walk-ins! Tacos/Pizza, Great English Speaking Food Introduction & Icebreaker First me and Jerome (a French iOS/Android Engineer here at Safie) introduced ourselves and then went straight to the icebreaker. Our Icebreaker was 2 Truths and 1 Lie, a personal favorite. The rules are write down 2 real things about yourself and 1 fake thing. The best/most fun are the ones that sound crazy but are actually true. My example was: I was friends with a princess in College I played American Football in High School <-- This is a lie! I have met Ayase Haruka Because we had 15+ people we split everyone into groups/teams and we had a lot of fun with it. I think the winner was when 1 participant played it perfectly and listed 3 injuries, making it impossible to guess 🤣 English Games We wanted to give beginners and other members not confident in their English skills a little more warm up so we prepared 2 games to play as a group. Pictionary and Word Train(Shiritori). Also classics from my Teacher days. We started easy and did some Food/Animals with Pictionary and then some harder ones like The Cloud ™ or our product Safie One. Everyone got them surprisingly quick! And for Word Train we started easy too with Animals and then to Programming words only and the programming one was way easier because everyone is in Tech. Free Talk Now that everyone was warmed up I wanted to open it up for some English conversation. We had a bit of time but 1 unfortunate thing cut our time a bit short. We had planned an hour or 2 of free talk but because of building maintenance we had to leave the building before 9. Next time we will leave more time to talk. On Reflection Overall good satisfaction was good but we didn't get to do much free talk so there wasn't much time to really practice English I think. It was due to an unfortunate scheduling conflict so hopefully next time we can all practice more! Some really positive feedback too! It was more fun than expected. Thank you so much for organizing such a great event! I'd like to be a staff next time if needed Thank you for the great event! I had much fun. It was unexpected that we could only stay until 9 PM because of the building's power outage. Next time, we should try to have more time for free talk. What's Next We are going to open it up to the whole company next time and hold it again this month. Also definitely making sure there is no building maintenance so we have more time.😅 Thanks to everyone who joined. If this sounds interesting to anyone we are hiring! 😉😉😉 open.talentio.com www.wantedly.com
こんにちは。「 Safie Connect 」のプロダクトマネージャーをしている企画本部 IoTソリューション部の坂元宏範です。 Safie Connectは、ドローンカメラをはじめHDMIで出力した映像データをLTE通信でSafieクラウドに伝送して、Live配信・クラウド録画を提供するサービスです。その開発の背景や商品の特長、リリース後の反響についてご紹介します。 開発の背景 プロダクトで実現すべきポイント Safie Connectの特長 1. 現場で誰もが手軽に使えること 2. 利用現場を選ばない 3. ユースケースに応じて選べる映像画質 サービスリリース後の反響 最後に 開発の背景 私は Saife Pocketシリーズ のプロダクトマネージャーも担当しており、Saife Pocket2の活用の調査のため、利用現場に立ち会っています。 その一環で、千葉県八千代市消防本部でSaife Pocket2の災害救助訓練に立ち合った時に、Safie Pocket2とともにドローンを使っていることを知りました。具体的には、災害現場上空にドローンを飛ばして災害現場の全体を撮影しつつ、Saife Pocket2を装着した救助部隊の映像を消防本部をはじめ各関係機関にリアルタイム配信して、現場の状況共有と救助全体の意思決定をしていました。映像が人命にかかわる意思決定に活用されていました。 その一方で、隊員が着けるSafie Pocket2とドローンの映像を同じ映像管理画面で見ることができず、スムーズな意思決定に重大な問題になりえるという話を伺いました。そして、Safieが提供している映像管理画面Safie Viewerは直感的な操作で使えるため、「ドローンの映像もSafie Viewerで管理したい!」、といった率直かつ強い要望をいただいたことがSafie Connectを開発するきっかけとなりました。 災害救助訓練の様子 Safie Pocket2で撮影した映像は、訓練後の振り返りに活用されている また、ドローンの活用という観点では、政府が推し進めている アナログ規制の見直し も開発スタートの追い風となりました。 「アナログ規制の見直し」では、人手不足の解消と生産性向上を目的として広い分野で規制が見直されています。たとえば建設・土木・インフラの分野では、従来では河川やダム等の維持修繕は目視・実地監査で行っていますが、ドローンや水中ロボット等の活用によって業務の効率化が一気に進むことが期待されていました。 建設・土木・インフラの各社には既にSafie Pocket2やSafie GO等のクラウドカメラが多く導入されています。今後ドローンを導入する際には、八千代消防と同様に、ドローンと他のカメラ映像の一元管理のニーズがあると確信していました。 ドローンで建設現場の進捗確認をする様子 実は、ドローンの映像を共有する方法としては、「ウェブ会議の仕組みを使ってリアルタイムで映像を伝送する」というやり方があります。 しかし、ユーザーヒアリングをしてみると、「映像伝送のためにはスケジューラーで会議を事前設定するのが手間である」、「映像の録画ができないため、撮影した映像を即座に見返せない」といった不満を抱えていました。 こうした既存の仕組みでの課題は、Safie Connectを開発する上でのヒントになりました。 プロダクトで実現すべきポイント Safie Connectの本格的な開発を前に、最低限の機能を実装したプロトタイプを製作し、現場に持ち込み実証実験を繰り返しました。 実証実験を通して、次にあげる3点がSafie Connectの仕様を決定する上で重要なポイントとなりました。 現場で誰もが手軽に使えること ドローンの飛行前には、機器のセットアップや安全確保のための、想定した以上の事前準備が必要でした。映像伝送のためのセットアップで現場の時間を奪ってしまい負担となることは避けるべきです。そのため、現場の負担となるセットアップが一切不要の、”電源を入れるだけで映像配信スタート”を実現させることが最重要なポイントとなりました。 利用現場を選ばない 建設・土木・インフラでドローンを利用する場合、その現場は山間部や湾岸エリアといった、街中とは異なり通信環境が悪い場所が多いです。実際に実証実験のために現場を訪れて、いざ電源を入れると、LTE通信が圏外で冷や汗をかいたこともありました。 ドローンを使う場所の特性上、通信の制約を限りなく排除して、より多くの現場で使えることも重要なポイントとなりました。 ユースケースに応じて選べる映像画質 ひとえにドローンの映像の活用と言っても、目的によって要求される映像画質は異なります。 例えば、災害時の巡視用途では、人やモノの動きを見るために、映像の鮮やかさよりも映像の滑らかさが重視されます。 一方で設備の点検では、コンクリートのひびや、ボルトのさびを点検するため、映像の滑らかさよりも映像の鮮明さが求められます。 映像の滑らかさと鮮明さの両方を満たそうとすると、映像のデータ量が大きくなり、LTE通信の帯域に納まらず、映像の切断や遅延の原因となります。つまり、LTE通信の制約のもと、映像の「滑らかさ」と「鮮明さ」のバランスを取り、目的に沿った映像画質を提供する必要があります。 Safie Connectの特長 前途のSafie Connectで達成すべき3つの仕様をどのような技術や手法で達成したかを説明します。 1. 現場で誰もが手軽に使えること Safie Connectアプリの開発は、現場での機器操作をミニマム化することを目指しました。 Safie Connectをドローンのコントローラに繋ぐ、Safie Connectの本体の電源を入れて、アプリを立ちあげるという、最短のアクションで自動的に映像がクラウドに伝送されます。 UIについては、使用頻度が高い機能アイコンに絞ってホーム画面に配置しており、一目で映像がクラウドに伝送されていることが判別がつくイラストアイコンを表示させました。 また、現場ですぐに使えるようにLTE通信は設定した状態で出荷している為、利用前に面倒な設定は不要です。細かなところになりますがケーブル類は事前に接続した状態で出荷するという工夫もしました。 2. 利用現場を選ばない LTE通信を利用したサービスである以上、安定した通信を確保することが良いユーザー体験に繋がります。 Safie Connectで映像伝送に使う通信デバイスの選定にあたっては、1台の通信デバイスに異なる通信キャリアの2つのSIMカードを同時に挿入できる機能を持つことを要件として、京セラ製ルーター(K5G-C-100A)を採用しました。 Safie Connectではdocomo回線を利用するMVNOとau回線を利用するMVNOのSIMを使っており、それぞれの回線の電波強度から、いずれかの回線を選択することができます。 また、LTE電波が安定しない場合、自動的にハードウェア内のストレージに映像データを一次的に保存しておき、電波が安定すると映像データをクラウドに伝送する機能を入れました。この機能により、映像録画データの欠損を抑えることがでますので、電波が安定しない環境でもユーザーは安心して使用することができます。 なお、京セラ製ルーター(K5G-C-100A)の採用は、通信の安定性以外にも、放熱ファン付きで長時間の使用でも熱暴走しにくい点、長時間のバッテリー駆動ができる点もポイントとなりました。 3. ユースケースに応じて選べる映像画質 映像の「滑らかさ」と「鮮明さ」のバランスを取った映像パラメーターの設定については、実際にドローンを飛ばして解像度やフレームレートを変えた映像サンプルを撮影して、ユーザーヒアリングを繰り返しました。 その結果、Safie Connectでは以下の4つの映像画質をユーザーが任意で設定出来る仕様にしました。また、電波が弱い環境下での使用を想定して、画質を落とした設定値も設けました。 映像の滑らかさ優先:HD画質/30fps、SD画質/30fps 映像の鮮明さ優先:FHD画質/5fps、HD画質/5fps ※SD画質→HD画質→FHD画質の順で解像度が高くなります ※fps(フレームレート)は数値が高いほど映像が滑らかになります 各映像画質はSafie Viewer上で設定変更が可能で、設定変更すると設定値はリアルタイム反映されます。 サービスリリース後の反響 2023年のリリース後、ありがたいことに、Safie Connectの利用者は順調に増えてきています。 Safie ViewerとSafie Connect間の双方向通話機能を追加しましたので、現場とのコミュニケーションツールとして活用されることを期待しています。 リリース後に想定外だったのは、ドローン以外の映像デバイスのリアルタイム伝送やクラウド録画をしたいという要望が多く寄せられたことです。 要望を受け、Safie Connectを活用した解決案を提案をしましたので、その一例を紹介します。 例えば、失火防止を目的とした現場の安全パトロールでサーモカメラを使っており、その映像をリアルタイムで遠隔監視したいという要望です。 これに対しては、HDMI出力できるサーモカメラとSafie Connectを繋ぎ、リアルタイム映像配信と通話機能を提案しました。サーモカメラで異常を検知した際に、その映像は遠隔からリアルタイムで確認でき、現場への指示をスムーズに出すことができるため、現場での迅速な対応が可能となります。 また、生産設備の稼動状況をモニタリングしているシステムを遠隔から確認したいという要望もありました。 こちらに対しては、設備の稼働状況をモニタリングしているパソコンにHDMIを挿し、Safie Connectでモニタリング映像をクラウドに伝送することを提案しました。責任者が遠隔からリアルタイムで稼動状況を見れるので、平常時の監視だけでなく、非常時にも遠隔から即座に稼動状況をチェックして、迅速な対応の指示出しが可能となります。 この他にもSafie Connectを活用の要望をいただいていますので、今後も様々は映像デバイスと連携したユースケースが広がっていくことを期待しています。 最後に Safie Connectは、ユーザーの声をきっかけに開発がスタートし、リリース後も「Safie Connectを使ってこんなことできないかな?」という相談を多くいただいております。 今後もユーザーの声をもとにSafie Connectのサービスを向上させていきますので、是非ご期待ください!
こんにちは。SafieでAndroidアプリの開発をしている渡部です。 先日、Safie社内でAWS Startersが開催されました。 AWS Startersは、AWS初学者を想定したハンズオンで、サービスの解説やグループワークを通じてAWSの基礎について学ぶことができます。 新卒エンジニアを対象としたハンズオンでしたが、社内で受講希望者を追加募集していたので自分も参加してみました! 今回はAWS Startersの大まかな内容と、当日の様子をお伝えしたいと思います。 AWS Startersについて セッションの流れ 今回の目標 AWSのサービスにふれる アーキテクチャ検討 終わりに AWS Startersについて 冒頭でもお伝えした通り、AWS StartersはAWS初学者を対象としたプログラムとなっています。 AWSサービスの解説 VPC、EC2、RDS等のハンズオン グループディスカッション AWSのエンジニアの方を社内にお招きして、上記の内容を直に受講することができます。 参加にあたって、社内では事前に「AWS Cloud Practitioner程度の知識が必要」とアナウンスされていました。 (ちなみに私はAWSについてはサービスの概要を知っている程度で、実務経験もありません🙂) 以下の動画を視聴してから当日に臨みました。 AWS Cloud Practitioner Essentials ※ AWS Startersの開催には、一定の条件を満たす必要があり開催が限定されています。 セッションの流れ 今回の目標 まずはセッションの全体の流れについての説明と、今回の目標について共有します。 今回の目標は、「堅牢なWebシステム(障害が起こってもサービスを提供できる環境)を作る」ことです。 その実現のためにどのようなサービスやアーキテクチャが必要かを学んでいきます。 AWSのサービスにふれる まず、今回のハンズオンで登場するAWSサービスについて説明を受けます。 Amazon EC2 AWS上で利用可能な仮想サーバー Amazon VPC(Virtual Private Cloud) AWS上にプライベートネットワーク空間を構築 Amazon Relational Database Service (RDS) データベースエンジンの選択が可能なマネージド・リレーショナルデータベースサービス Elastic Load Balancing(ELB) AWS上のロードバランシングサービス AWS Auto Scaling EC2 インスタンスを負荷またはスケジュールに応じて自動増減 リージョンの概念やセキュリティグループ(ファイヤーウォール)などの基礎的な部分から解説を受けました。 解説の後、実際に手を動かしながらサービスの構築を体験します。各参加者に当日限りのデモアカウントが用意されており、本番と同じ操作感で作業を進められます。 今回のデモの具体的なゴールは、【WordPressを用いながら、AWS上でスケーラブルなWebシステムの構築を行う】ことです。 そのための手順が細かく解説され、一つ一つ順を追って構築していきます。 大まかな作業としては、以下のように進めていきました。 (WordPressの環境はあらかじめ用意されていたので、初期設定のみを行いました。) 基本的な環境構築 VPCとサブネットの作成 EC2インスタンスの作成 データベースを準備 RDSの作成 サーバー負荷を分散 ELBの作成 構成の冗長化 AMIを作成し、EC2のマシンイメージを取得 AMIとはAmazon Machine Imageの略で、EC2インスタンスを起動する際のテンプレートのような役割を果たします。 AMIを用いてEC2インスタンスを複製 RDSをマルチAZ配置 AZとはアヴェイラビリティゾーンの略で、AWSのデータセンターのグループを指します。 RDSのマルチAZ配置とは、元データのコピーを異なるAZに配置して管理するアクションです。 以上の作業の結果、図のような環境ができあがります。 出典:AWS Workshop Studio スケーラブルウェブサイト構築 ハンズオン Webページ https://catalog.us-east-1.prod.workshops.aws/workshops/47782ec0-8e8c-41e8-b873-9da91e822b36/ja-JP/hands-on/phase9 (2024年6月30日アクセス) EC2インスタンスやRDSを、複数のAZに配置しています。これにより万が一どこかのAZがダウンしたとしてもサービスを提供し続けられますね。 このように、今回の目標である「堅牢なWebシステム」実現するために、可用性を高めた構成となりました。 アーキテクチャ検討 ここまでは個人による作業でしたが、次にグループに分かれてのディスカッションが行われました。 今日学んだことを踏まえて、課題が発表されます。 課題の詳細は割愛しますが、概要としましては、「提示された要件を叶えて、且つ可用性も高まる、AWSのアーキテクチャを考える」といった内容です。 各グループに分かれ、ディスカッションを行いました。 今回は初学者向けのハンズオンではありますが、参加者の中にはAWSを実務で使用している方やサービスに詳しい方もいて、講義では触れなかったサービスも積極的に構成に取り入れるグループも多かったです。 グループでの検討が完了したら、各グループが参加者の前で発表を行い、講師の方からフィードバックを頂きました。 複数のAZにEC2やRDSを配置するといった基本的な構造はどのグループも取り入れていましたが、セキュリティの担保やログの保持については異なる案がいくつも出てきて、大変勉強になりました。 ちなみに私が参加したグループでは、ユーザー側が読み込む必要のある画像ファイルなどは Amazon S3 で管理してはどうかという案が出ました。 S3は費用対効果が優れつつ、デフォルトで3つのAZにデータを保存できるので、可用性の面でも適したサービスとなっています。 各チームの発表(一部) 終わりに 1日かけてのハンズオンでしたが、楽しくためになる時間を過ごせました。 解説を受けてからすぐに実際に手を動かしてサービスに触れることで、理解が深まると同時にAWSサービスへ触れるハードルが下がったような気がします。 また自分にとっては、日頃はモバイル以外の開発にあまり関わっていないため、サーバーのアーキテクチャについて学べたこともとても勉強になりました。 Safie社内では定期的にエンジニアのためのイベントを開催しています。 今後も参加する機会があったら、ぜひまたお伝えしたく思います。 ここまで読んでくださりありがとうございました。
ワークショップの目的 ワークショップの内容 ワークショップの実施 今後に向けて セーフィー株式会社テックリードの鈴木敦志です。 セーフィーでは普段の業務で使う技術だけでなく、エンジニアのスキルアップのために様々な取り組みを行っています。先日は、IoT機器の開発に欠かせないマイコン (マイクロコントローラー) について理解を深めるため、M5Stack社の製品を使ったワークショップを開発部で開催しました。 ワークショップの目的 セーフィーではクラウドカメラサービスを提供しており、カメラメーカーの提供するLinux等OS・ソフトウェアをベースに組み込みソフトウェアを開発しています。一方でより小型のIoT機器の実装にはEspressif社のESP32シリーズをはじめとするインターネット接続が可能なマイコンを用いることができます。 今回のワークショップの目的は以下の通りです。 マイコンの扱いに慣れること Linux/SBC (Single Board Computer) と異なるマイコンの特性を理解すること IoT連携などを行う際の選択肢として知見を得ること ワークショップの内容 今回はM5Stack社の Atom S3 Lite というマイコンモジュールと 人感センサーユニット を使用し、人の動きを検知したらSlackに通知を送る装置を実装しました。 Atom S3 Lite (右) およびPIRセンサーユニット (左) プログラミングにはM5Stack用の開発環境であるUIFlow2を使用しました。BlockyによるビジュアルプログラミングとMicroPython (Pythonのマイコン向け環境) が利用できますが、今回の実習の参加者はQAエンジニア等プログラマー以外の職種を含むためBlockyを用いて進行しました。UIFlow2はWeb Serial APIを用い、マイコンへの書き込みまで含めブラウザ上で開発を完結させることができます。 プログラムの動作は下記の流れになります。 人感センサーユニットの出力をポーリングし値の変化を待つ Wifi経由でSlackのWebhookにHTTPリクエストを送信 Slack Workflowで通知メッセージを表示 UIFlow上のプログラム Pythonソースコード pre.code{ white-space: pre; overflow: auto; max-height: 400px; } import os, sys, io import M5 from M5 import * from unit import PIRUnit import network import requests import time wlan = None http_req = None pir_0 = None pir_now = None pir_old = None def setup (): global wlan, http_req, pir_0, pir_now, pir_old M5.begin() pir_0 = PIRUnit(( 1 , 2 )) wlan = network.WLAN(network.STA_IF) def loop (): global wlan, http_req, pir_0, pir_now, pir_old M5.update() pir_now = pir_0.get_status() if pir_now and not pir_old: http_req = requests.post( 'https://hooks.slack.com/triggers/XXXXXXXXXXX/XXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' , json={}, headers={}) print (http_req.status_code) http_req.close() pir_old = pir_now time.sleep( 1 ) if __name__ == '__main__' : try : setup() while True : loop() except ( Exception , KeyboardInterrupt ) as e: try : from utility import print_error_msg print_error_msg(e) except ImportError : print ( "please update to latest firmware" ) ワークショップの実施 ワークショップの様子 今回のワークショップは、第1開発部のサーバー、インフラ、QCDチームを中心とするメンバーと、第2開発部のフロントエンドおよびモバイルチームのメンバー、それぞれ約20名ずつの2グループに分けて実施されました。 ワークショップは2時間程度で行われ、参加者の多くはマイコン開発の経験がほとんどありませんでしたが、全員が最後までプログラムを完成させることができました。 今後に向けて 今回のワークショップでは、普段触れることの少ないマイコンを使ったIoTデバイスの開発を体験することができました。マイコンは計算リソースが限られまた開発手法もPC等向け開発と異なるノウハウが必要ですが、こうした経験は、クラウドやWebの開発とはまた違った視点を与えてくれるものと思います。 また、IoTの分野ではマイコンを使ったデバイスが数多く登場しています。今回のようにマイコンの特性を理解しておくことでIoTシステムを設計する際の選択肢が広がります。例えばSafieクラウドと連携するIoTシステムの設計を行うとき、PCやSBC・スマートフォン等を使用するかわりにマイコンを使用することで安価で小型かつ長時間のバッテリー駆動を実現できます。 技術の幅を広げることは、エンジニアにとって重要なことだと考えています。今回のようなワークショップを通じて、普段の業務では触れない技術に触れることで、新しいアイデアが生まれるかもしれません。引き続き、セーフィーでは技術的なチャレンジを続けていきたいと思います。
はじめに 使うライブラリ 環境構築 python環境 ライブラリインストール コード実装 動画ファイルから音声ファイルを作る 音声ファイルから文字起こしする Web UIを作る コードを組み合わせる(完成版) 実行 所感 最後に はじめに セーフィー株式会社 開発本部 第3開発部 AIVisionグループで画像認識AIの開発エンジニアをしている土井 慎也です。 今回は、前回の記事で題材にしたGradioを使用して動画から文字起こしを行うアプリを簡単に作ってみたいと思います。 engineers.safie.link ローカルでAIを動かしますが、GPUは不要です。 また、オフラインでも実行は可能なので、セキュリティー的にも安心です。 使うライブラリ gradio WEBフレームワーク 簡単にWeb UIが作れる faster-whisper 音声認識 OpenAIのWhisperモデルを高速化したもの moviepy 動画編集 今回は動画から音声のみを抽出するために使用 環境構築 python環境 python3.8以上が実行可能な環境を用意します。 ライブラリインストール pip install faster-whisper gradio moviepy コード実装 動画ファイルから音声ファイルを作る input_video.mp4から音声のみ取り出したtemp_audio.mp3を作成 from moviepy.editor import VideoFileClip video_path = "input_video.mp4" output_audio_path = "temp_audio.mp3" video = VideoFileClip(video_path) video.audio.write_audiofile(output_audio_path) 音声ファイルから文字起こしする 今回はCPUで推論を行うため、deviceはcpu、compute_typeはint8を指定 GPUの場合はcuda、compute_typeはfloat16 from faster_whisper import WhisperModel model = WhisperModel( "large-v3" , device= "cpu" , compute_type= "int8" ) segments, info = model.transcribe( output_audio_path, beam_size= 5 , vad_filter= True , ) response = "" for segment in segments: response += "[%.2fs -> %.2fs] %s \n " % (segment.start, segment.end, segment.text) print (response) Web UIを作る 入力は動画ファイル、出力はテキストを想定 import gradio as gr def main (video_path: str ): return "" demo = gr.Interface(fn=main, inputs=gr.Video(), outputs= "textarea" ) if __name__ == "__main__" : demo.launch() コードを組み合わせる(完成版) from faster_whisper import WhisperModel from moviepy.editor import VideoFileClip import gradio as gr import os model = WhisperModel( "large-v3" , device= "cpu" , compute_type= "int8" ) def video_to_audio (video_path: str , output_audio_path: str ): video = VideoFileClip(video_path) video.audio.write_audiofile(output_audio_path) def audio_to_text (audio_path: str ): segments, info = model.transcribe( audio_path, beam_size= 5 , vad_filter= True , without_timestamps= True , ) print ( "Detected language '%s' with probability %f" % (info.language, info.language_probability) ) response = "" for segment in segments: response += "[%.2fs -> %.2fs] %s \n " % (segment.start, segment.end, segment.text) return response def main (video_path: str ): output_audio_path = "temp_audio.mp3" video_to_audio(video_path, output_audio_path) response = audio_to_text(output_audio_path) os.remove(output_audio_path) return response demo = gr.Interface(fn=main, inputs=gr.Video(), outputs= "textarea" ) if __name__ == "__main__" : demo.launch() 実行 pythonで作成したソースコードファイルを実行してしばらくたつと、以下のように表示されるので、ブラウザで http://127.0.0.1:7860 を開きます $ python main.py Running on local URL: http://127.0.0.1:7860 To create a public link, set `share=True` in `launch()`. そうすると、このような画面が出るので、左のvideo_pathで動画を選択して、submitボタンを押すと文字起こしの処理が実行されます。 しばらく、そのまま待っていると、右のテキストエリアに結果が出力されます。 所感 簡単に実装できて実用的なアプリを考えたときに、whisperを使用した文字起こしアプリを選びましたが、思った以上に簡単に実装できました。 ここからさらに、LLMで要約したり、翻訳機能を使って多言語対応とかすると、さらに便利にできると思うので、近いうちにそういった改良もしてみたいと思います。 最後に セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。
こんにちは、セーフィーでサーバーサイドの開発をしてる金成です。 今回は、ArchlinuxのインストールRTAを行ってみたいと思います。 Archlinuxと会社の業務とはほぼ関係ありませんが、やってみて色々勉強になったので共有させてください。 Archlinuxとは何か Archlinux install RTA レギュレーションについて Archlinuxのインストール手順について 1. 起動モードの確認 2. インターネットへの接続 3. システムクロックの更新 4. パーティションを作成 5. パーティションをフォーマットする 6. ファイルシステムのマウント 7. 必須パッケージのインストール 8. fstabの生成 9. chrootする 10. タイムゾーンの設定 11. ローカリゼーション 12. ホスト名の設定 13. ルートパスワードの設定 14. ブートローダーのインストール、設定 15. 再起動 今回実行するコマンドのまとめ 記録 完走した感想 参考 Archlinuxとは何か Archlinuxとは、Linuxディストリビューションのうちの一つです。 そもそもLinuxディストリビューションとは、OSの中核となるカーネルとその他のソフトウェア群を一つにまとめ、インストール・利用しやすくしたものです。 概ねLinuxディストリビューションにはパッケージマネージャー(apt, yum や dnfなど)も同梱され、ソフトウェアの維持管理がしやすいようになっています。 有名なディストリビューションは、ubuntuなどDebianを祖とするものやFedoraやcentOSなど Red Hat Linuxの系譜をひくものがあります。それ以外だと、OpenSUSEなどのslakwareの系譜のディストリビューションやGentoo Linuxなど変わったものも存在します。普段の開発では、ubuntuやcentOSなどにお世話になることが多いかもしれません。 今回利用するArchlinuxは、Debian系やRedHat系のどちらにも属さず、商用に利用されることはほとんどないOSです。 個人のデスクトップPC用に使う、自宅サーバーに使うなどが主な用途です。 自分で調べて解決できるユーザーをターゲットとしたOSのため、ユーザーフレンドリーでなく(GUIもデフォルトだとない)決して扱いやすいものではありませんが、 軽量で不必要なものの入っていないためカスタマイズ性が高く、好きなだけイジることができるため、自分好みのPCを組むことができるOSとなってます。 *1 Archlinux install RTA Archlinux Install RTAは、Archlinuxをライブ起動してからインストールし、各種設定を完了するまでの時間を競うバトルです。 先程書いた通り、Archlinuxは軽量なOSのため、インストールした時点ではほぼほぼ何も設定されいません。ディスクのパーティショニング、時刻の設定、ルートユーザーの作成 など一通りの初期設定をインストール時に行う必要があります。USBに焼いたisoファイルで起動して、各初期設定を終えてリブートするまでの時間を競うのが今回やることです。 今までネット上でゲリラ的に開催されていますが、決まったルールはないので、今回はこちらでルールを決めて行いたいと思います。 *2 レギュレーションについて 実機へのインストールだと、マシンごとの差が大きいため、仮想環境を使います。 Virtual Boxは筆者の使用してるPCだと起動できないので、今回はUTMを利用します。 レギュレーションは以下の通りです。 起動モードはUEFIモード インターネットへは接続できる状態にある partionは、ESPとルートの二つを作成する ローカルタイムの設定をする 言語設定をする /etc/hostnameはセットする bootloaderは何を使っても良い(今回は、systemd-bootを使う) live起動で、ターミナルが入力できるようになったらタイマースタート 再起動し、uname -r の表示がされたらタイマーストップ 仮想環境のUTMを使う utmの設定は、x86_64 アーキテクチャ、メモリ4Mib, ストレージ64GB, コア数2, システムStandard PC (Q35 + ICH9 2009) とする Archlinuxのインストール手順について Archlinuxのインストール手順については公式サイトのものを利用します。 始める前に、今回のインストール手順について解説していきます。なお、Archlinuxのインストールメディアはすでに準備してあるものとします。 以下コマンドをうち、実行完了するまでを競います 1. 起動モードの確認 UEFI変数(OSとファームウェアが情報を交換するために定義された変数)を、efivarsディレクトリを表示することで確認します。 ls /sys/firmware/efi/efivars # 起動モードの確認。正しく表示されればUEFIモード 2. インターネットへの接続 ネットワークインターフェースの有効化の確認と、ping での接続確認を行います ip link ping -c 3 archlinux.jp 3. システムクロックの更新 ライブ環境では、インターネットの接続が確立されると時刻が同期されます。 下記のコマンドで現在の設定を確認します timedatectl status 4. パーティションを作成 パーティショニングとは、ストレージデバイス(HDDやSSDなどの記録装置)を分割することを指します。論理的に分割された領域をパーティションといいいます。 これにより、1のハードディスクに複数のファイルシステムをもつことや用途別にパーティション作成することが可能になります。 パーティションの情報を格納するパーティションテーブルには、GPTとMBRの二つがありますが、今回はGPTを使用します Archlinuxでは、インストールに以下のパーティションが必要になります ルートディレクトリ / のパーティション EFIシステムパーティション (OSの起動やブートローダーの管理に利用) 今回はpartedコマンドで、0%-551MibをEFIシステムパーティション用に、残りをルートディレクリのパーティション用に分割します。 parted /dev/sda # インタラクティブモードでpartedを実行 (parted) mklabel gpt # PTにGPTを指定 (parted) mkpart ESP fat32 0 % 551Mib # EFIシステムパーティションを作成 (parted) set 1 esp on # パーティションを起動可能に (parted) mkpart primary ext4 551Mib 100 % # ルートディレクトリのパーティションを作成 (parted) q 5. パーティションをフォーマットする 作成したパーティションは、適切なファイルシステムでフォーマットされる必要があります。 下記のコマンドで作成したファイルシステムをフォーマットします。 mkfs.fat -F 32 /dev/sda1 # EFIシステムパーティションのフォーマット mkfs.ext4 /dev/sda2 # ルートディレクトリのパーティションのフォーマット 6. ファイルシステムのマウント マウントとは、OSにストレージやパーティションにあるファイルシステムを認識させ、アクセス可能にすることを指します。 下記のコマンドで、EFIシステムパーティション・ルートディレクトリのパーティションのファイルシステムを認識させ、アクセス可能にします mount /dev/sda2 /mnt # ルートディレクトリ mount --mkdir /dev/sda1 /mnt/boot # EFIシステムパーティション 7. 必須パッケージのインストール baseパッケージ、カーネル、ファームウェアのインストールをします. このコマンドでは、テキストエディタやネットワークを設定するソフトウェア(ネットワークマネージャー etc) のインストールは含まれていません。 場合によっては、インストール後にインターネットに接続できない状態になる可能性があります。通常であれば、エディタやネットワークマネージャーもインストールすることをお勧めします。 pacstrap -K /mnt base linux linux-firmware 8. fstabの生成 fstabファイルとは、パーティションやブロックデバイスをどうやってファイルシステムにマウントするかを記述したファイルです。 下記のコマンドでfstabを生成します。 genfstab -U /mnt >> /mnt/etc/fstab 9. chrootする 新しくインストールしたシステムにchrootします arch-chroot /mnt 10. タイムゾーンの設定 下記のコマンドでタイムゾーンを設定します。今回は Asia/Tokyo にします また、ハードウェアクロックをシステムの現在時刻に設定します ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime hwclock --systohc 11. ローカリゼーション en_US.UTF-8 のロケールの生成、環境変数の設定をします sed -i -e "171 s/^#//" /etc/locale.gen # 171行目に#en_US.UTF-8があるので、先頭の#を削除 locale-gen echo LANG=en_US.UTF-8 > /etc/locale.conf 12. ホスト名の設定 ホスト名とは、ネットワーク上でマシンを識別するために作られる名前です 基本的に好きな名前を使って大丈夫です 今回は入力速度を考慮して、safieとします echo safie > /etc/hostname 13. ルートパスワードの設定 パスワードを設定します passwd 14. ブートローダーのインストール、設定 ブートローダーとはパソコンの起動時に呼び出されるプログラムで、ストレージからOSを呼び出して起動するものです。 今回は、systemd-bootがbaseパッケージとともにインストールされるため、こちらを利用します。 下記のコマンドで、EFIブートマネージャーのインストール、設定を行います。 bootctl install # bootloaderのインストール cat << EOF > /boot/loader/loader.conf # 設定ファイルの作成. default arch timeout 0 editor no EOF cat << EOF > /boot/loader/entries/arch.conf # ローダーの追加 title archlinux linux /vmlinuz-linux initrd /initramfs-linux.img options root=UUID= $( blkid -s UUID -o value /dev/sda2 ) rw EOF 15. 再起動 exitで、chroot環境から抜けて、再起動します。任意ですが、アンマウントも実行します exit # live環境に戻る umount -R /mnt # アンマウント poweroff # シャットダウン. 停止後、インストールメディアを取り除きUTMで再起動。 rootユーザーでログインし、uname でカーネルのバージョンを表示したところでタイマーストップで す。 uname -r 今回実行するコマンドのまとめ ls /sys/firmware/efi/efivars # 起動モードの確認。正しく表示されればUEFIモード ip link # ネットワークインターフェースの状態を確認 ping -c 3 archlinux.jp # archlinux.jpとの疎通確認 timedatectl status # 時刻確認 parted /dev/sda # パーティション作成 (parted) mklabel gpt (parted) mkpart ESP fat32 0 % 551Mib (parted) set 1 esp on (parted) mkpart primary ext4 551Mib 100 % (parted) q mkfs.fat -F 32 /dev/sda1 # フォーマット mkfs.ext4 /dev/sda2 mount /dev/sda2 /mnt # マウント mount --mkdir /dev/sda1 /mnt/boot pacstrap -K /mnt base linux linux-firmware genfstab -U /mnt >> /mnt/etc/fstab arch-chroot /mnt ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime hwclock --systohc sed -i -e "171 s/^#//" /etc/locale.gen locale-gen echo LANG=en_US.UTF-8 > /etc/locale.conf echo safie > /etc/hostname # 入力速度を考慮してsafieに passwd bootctl install # bootloaderのインストール cat << EOF > /boot/loader/loader.conf # 設定ファイルの作成。起動時にarch.confを見るように default arch timeout 0 editor no EOF cat << EOF > /boot/loader/entries/arch.conf # ローダーエントリの追加 title archlinux linux /vmlinuz-linux initrd /initramfs-linux.img options root=UUID= $( blkid -s UUID -o value /dev/sda2 ) rw EOF exit umount -R /mnt # マウントを解除 poweroff # 電源を切る # utmでCD/DVDを消去し起動 # rootでログイン uname -r # 表示されたらタイマーストップ 記録 記録は、11:07.19 でした 完走した感想 完走した感想ですが、キーボードの入力速度とコマンドをどれだけ記憶してるかが勝負を分つと思います。 OSのインストールに関わるコマンドやツールを色々と知ることができたので楽しかったです。 ちなみに、このレギュレーションでやってる人はいなかったので私が世界一です(N=1)。皆さんの挑戦をお待ちしております。 最後になりますが、セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。 参考 Arch Linux - ArchWiki インストールガイド - ArchWiki Parted User’s Manual *1 : ただ、archlinuxのwikiは説明が充実しており、わかりやすいです。 *2 : インストールのスピードを競う必要性は全くありません。
はじめに こんにちは、セーフィー株式会社で iOS エンジニアをしている篁です。 普段は Safie Viewer の iOS アプリを開発しています。 およそ1年半前に Registered Scrum Master 研修を受ける機会に恵まれ、それ以来 iOS チームのスクラムマスターをしています。 私のチームでは、1年前からスプリントの振り返りに Sailboat Retrospective というフレームワークを利用し始めました。利用してみていくつかのメリットを感じられたため、この記事で紹介させていただきます。 はじめに Sailboat Retrospective とは 導入の経緯 振り返りのアジェンダ アイスブレイク (5分) 前回のスプリントで設定したアクションの振り返り (1分) 今スプリントで完了できなかったタスクの確認 (5分) 今スプリントの振り返り (5〜10分) 付箋の内容について各メンバーから共有してもらう (10分) 次回のスプリントで具体的にアクションすべき付箋を決める (1分) 具体的なネクストアクションを決める (2〜4分) まとめ 参考 Sailboat Retrospective とは Sailboat Retrospective は、スクラムチームがプロジェクトの振り返りを行うためのフレームワークの一つです。 プロジェクトをヨットでの航海に見立て、チーム (ヨットの乗組員) が目標に向かって進むための推進力 (追い風) や障害 (錨) などを視覚的に表現します。 私のチームで用いている具体的な要素とそのメタファーは以下の通りです。 要素 メタファー 内容 Goal ヨットが向かう島 スプリントゴール Wind ヨットを進める追い風 プロジェクトを前進させるポジティブな要素や成功要因。例えば、良いコミュニケーション、良いリサーチ、なんでもOK Sun 行き先を照らす太陽 嬉しかったこと、良かったこと。goal と関係なくてもOK Cloud 影を落とす雲 モヤモヤ気になることなんでも Anchor ヨットの動きを妨げる錨 チームの進行を妨げる課題や問題点。例えば、コミュニケーション不足、タスクに集中する時間が少ない、その他なんでもOK Reef (小) 海中の小さな岩礁、目には見えないが進行を妨げる可能性がある 近い将来、チームに悪影響を及ぼす可能性のあるリスクや障害 Reef (大) 海中の大きな岩礁、目には見えないが進行を妨げる可能性がある 将来、チームに悪影響を及ぼす可能性のあるリスクや障害 表にある要素以外にも、チームでオリジナルの要素を追加したり、不要な要素を除いたりして、チームに最適化していくのが良いと思います。 私のチームでは、普段から Figma の利用に慣れていたこともあり、FigJam の Sailboat Retro テンプレート を利用しています。他にも楽しいテンプレートが色々とあるようです。 各要素の振り返りを視覚的に見やすく整理することで、改善点の明確化と次のアクションプランの策定がスムーズに進みます。 最終的には、付箋やリアクションが貼られた以下のような状態になります。詳細内容については、後述の振り返りのアジェンダで説明します。 導入の経緯 私のチームでは、以前は KPT フレームワークを利用して振り返りを行っていました。 KPT は Keep、Problem、Try の3つの要素に対して振り返りを行います。 要素 内容 Keep 上手くいっていることや引き続き実施したいこと Problem 課題や問題点、改善が必要なこと Try 試してみたい新しいアイディアや改善策 KPT は、短時間で振り返りを行うのに適しており、チームがすぐに具体的なアクションプランを立てることができます。 しかし、手軽に振り返りができる一方で、以下の課題を感じていました。 反省会の意味合いが強くなり、固い雰囲気になる 要素が大きく分類されているため、少し抽象的な意見が出やすい 毎回似たような振り返りになりがち 課題を共有したものの解決されずに長く残り続ける そんなときに Sailboat Retrospective フレームワークを知り、上記課題を解決することを期待して導入に至りました。 導入時には、KPT を利用していたチーム状況が似ていることもあり、 こちらの記事 を参考にさせていただき、振り返り要素などを参考にしました。 振り返りのアジェンダ 私のチームでの振り返りのアジェンダを紹介します。 ファシリテータはスクラムマスターが務めますが、慣れてきたらメンバーで当番制にしても良いかもしれません。 アイスブレイク (5分) チームメンバーの調子と1つの話題について付箋で共有してもらい、ワイワイ話します。 前回のスプリントで設定したアクションの振り返り (1分) 前回のスプリントで設定した、今スプリントで実施すべきアクションを達成できたかどうかを確認します。 達成できていたら付箋を剥がし、達成できなかった場合は原因を議論して、継続アクションとするか別のアクションにするかを決定します。 今スプリントで完了できなかったタスクの確認 (5分) スプリントで完了できなかったタスクがあれば、担当者に進捗と完了できなかった原因などを共有してもらいます。 セーフィーでは Backlog でタスクを管理しているため、実際にチケットを眺めながら確認しています。このときにベロシティなどについてもチームで共有します。 今スプリントの振り返り (5〜10分) まずは5分間を計測し、各メンバーが今スプリントでの出来事や感じたことを自由に付箋に書いて各要素に貼っていきます。時間が足りないときは適宜延長します。 FigJam ではタイマーと連動した BGM を流すことができ、作業中も静まり返らないようにするため、個人的にお気に入りの機能です。 付箋の内容について各メンバーから共有してもらう (10分) 付箋を順番に見ていき、付箋を貼ったメンバーから内容を共有してもらいます。 私のチームではポジティブな要素から順番に共有しています。他のメンバーは良いところを褒めたり、リアクションスタンプを付箋に追加して、メンバー全員が議論に参加するという雰囲気作りを意識します。 次回のスプリントで具体的にアクションすべき付箋を決める (1分) 1分間を計測し、各自が次回のスプリントで具体的なアクションとして取り組むべきと考える付箋に投票します。 投票数の制限はなく、一人あたり何票投票してもOKです。このとき、投票者が分かるようにしています。FigJam では自分のアイコンのスタンプを利用できます。 具体的なネクストアクションを決める (2〜4分) 1票以上投票されている付箋の内容について議論し、具体的なネクストアクションを決めます。 議論の内容や決定したアクションは、別枠を設けて新しい付箋に書いて貼ります。実行しづらい抽象的なアクションを避けて、可能な限り具体的に実行しやすいアクションにします。たとえば、チームのルールとしてドキュメント化、タスクとしてチケットの作成、チームで相談したいのでカレンダーに追加、などです。 まとめ これまで Sailboat Retrospective での振り返りを行ってきましたが、先述した課題は解決され、以前よりチームの一体感が醸成されたと感じています。 Sailboat Retrospective には、改めて以下のメリットがあると感じています。 視覚的な要素を含むことにより、チームメンバーが直感的に理解しやすく、リアクティブに楽しく振り返りできる 問題点 (Anchor) や推進力 (Wind) など、プラス面とマイナス面をバランスよく議論することにより、チームがどの方向に進むのか、何が進捗を妨げているのかを明確に把握できる 具体的なネクストアクションを設定することで、課題が長い間放置されず解決に進む ルールも簡単なので、振り返りを少し変えてみたいというチームにオススメしたいです。今後もより良い振り返りに改善していきます。 参考 Sailboat Retro FigJam board 脱 KPT 法で楽しく有意義に振り返り! Sailboat Retrospective Backlog
はじめに はじめまして!事業戦略部の松本です。ビジネスサイドでSafie API関連のサポートを担当しています。 本記事ではSafie APIとDeep Learningを利用して、飼い猫達を画像分類し給仕DXをしたのでお伝えしていきたいと思います。 はじめに やりたいこと Safie APIとは どうするか 画像取得APIを実行してみる 猫を学習 モデルを学習する関数 実行するタイミングを決める いつでもどこでも見られるようにする やりたいこと 我が家には飼い猫のチャチャ(メス: 2歳7か月)とクロ(オス: 1歳11か月)が暮らしています。 サビ猫のチャチャ 黒猫のクロ 当初はチャチャを1匹でお迎えしていましたが、YouTubeなどで2匹の飼い猫が仲良く暮らしているのを見て憧れ、1年ほど後にクロもお迎えすることにしました。 初めての多頭飼い(複数のペットを飼うこと)だったので、事前に色々予習をしました。生活回りのものは分けた方がいいということだったので、餌やり器や水やり器、トイレなども別々で用意していたのですが、幸か不幸か2匹は同じものを利用しているようでした。 そこで出てきたのが、チャチャとクロが「 それぞれどのくらいの餌を食べているのか分からない問題 」(以後、猫餌問題)でした。 今までチャチャ1匹の時は、食べている餌の量でその日の体調の良し悪しが分かっていたのですが、2匹が同じ餌やり器を共有することでそれが分からなくなってしまったのです…...。 そこで、Safie APIとディープラーニングを組み合わせて、チャチャとクロのそれぞれがどのくらい餌を食べているのか判別するようにしました! Safie APIとは Safieでは、ユーザ様がSafieカメラで撮影した映像や静止画を自社のツールに組み込んだり、AIerなどが提供する映像解析と連携させるために、 オープンAPI であるSafie APIを提供しています。 Safie APIでは映像や静止画などのバイナリデータの他、Safie OneなどのエッジAIを搭載したカメラで利用できるAI-App人数カウントのデータも取得できます。その他にも取得できるデータは様々あり、取得方法も併せて 開発者向けサイトSafie Developers に纏まっています。 もちろんデータの利用には映像データ所有者のユーザ様の事前許諾が必要で、認可/ 認証にはOAuth2.0/ APIキーを採用しています。 どうするか 今回は猫餌問題解決のため、「画像取得API」を利用することにしました。このAPIでは実行したタイミングの最新の静止画が取得出来る他、指定した過去の任意の時点の静止画を取得することもできます(録画プランの範囲内に限ります)。 我が家では電動で猫の餌が出る餌やり器を利用しています。この餌やり器をSafieカメラで撮影し、そこに映った猫がチャチャなのかクロなのかを TensorFlow を利用し画像分類して判別できるようにしたいと思います。 画像取得APIを実行してみる まず画像取得APIを利用して、カメラの静止画を取得してみます。Safie APIを利用するにはまず Safie Developers でアプリケーションを作成します。 Safie APIでは認証方式をOAuth2.0とAPIキーの2種類から選択できます。APIキー方式ではトライアル利用もできるため、今回はAPIキー方式を採用します。 APIキーの発行もSafie Develoeprsから実施します。 実際の画像取得APIの実行方法については、 APIリファレンス で仕様を確認できます。 リファレンスではAPIリクエスト時のエンドポイントやパラメータ、レスポンスの内容が確認できます。 画像取得API で必要なパラメータはdevice_idとtimestampで、timestampを省略すると直近の静止画がレスポンスで返ります。device_idはSafieカメラごとに固有のIDでシリアルナンバーとは異なり、 デバイス一覧取得API で確認できます。 import requests api_key = "YOUR_API_KEY_HERE" device_id = "YOUR_DEVICE_ID_HERE" url = f "https://openapi.safie.link/v2/devices/{device_id}/image" headers = { "Safie-Api-Key" : api_key} try : res = requests.get(url, headers=headers) res.raise_for_status() except : print (f "Error! Status code: {res.status_code}" ) else : with open ( "./safie_image.jpg" , "wb" ) as f: f.write(res.content) APIを実行する際はリクエストヘッダにAPIキーを付与します。実行したところ静止画が保存されていることが確認できました。 保存される静止画のイメージ。チャチャが餌を食べているところ。 猫を学習 次に保存した写真を分類するための前準備として「チャチャ」と「クロ」と「猫が映っていない」の3パターンをTensorFlowを使って機械学習させていきます。 具体的には、それぞれの食事中の写真を数百枚ずつほど用意しました。 ここは手動となるので地道にSafie Viewerとにらめっこしながら作業しますが、Safieの多くのカメラには動きのあった時間を検知する「モーション検知機能」が標準で用意されているのでそこまで大変な作業ではありませんでした。 猫は部屋が暗い夜間でも餌を食べます。Safieの多くのカメラには真っ暗な環境でも白黒の映像を撮影できる「IR機能」が備わっているため、今回はIR機能を常時ONにし、学習させる画像データもIRがONの白黒のものにしました。 これらの写真を使ってデータセットの作成・モデルの構築をしていきます。 こちらのサイト を参考にしました。 1.データセットの作成。データ拡張のため画像の回転・反転を行っています。 from PIL import Image import glob import numpy as np from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES = True classes = [ "chacha" , "kuro" , "na" ] num_classes = len (classes) image_size = 64 num_testdata = 25 X_train = [] X_test = [] y_train = [] y_test = [] for index, classlabel in enumerate (classes): photos_dir = "./" + classlabel files = glob.glob(photos_dir + "/*.jpg" ) for i, file in enumerate (files): image = Image.open( file ) image = image.convert( "RGB" ) image = image.resize((image_size, image_size)) data = np.asarray(image) if i < num_testdata: X_test.append(data) y_test.append(index) else : for angle in range (- 20 , 20 , 5 ): img_r = image.rotate(angle) data = np.asarray(img_r) X_train.append(data) y_train.append(index) img_trains = img_r.transpose(Image.FLIP_LEFT_RIGHT) data = np.asarray(img_trains) X_train.append(data) y_train.append(index) X_train = np.array(X_train) X_test = np.array(X_test) y_train = np.array(y_train) y_test = np.array(y_test) xy = (X_train, X_test, y_train, y_test) np.save( "./chacha_kuro_datasets.npy" , xy) 2.モデルの作成(一部)。作成したデータセットから猫の判別に利用するKerasモデルを作成しています。 # データを読み込む関数 def load_data (): X_train, X_test, y_train, y_test = np.load( "./chacha_kuro_datasets.npy" , allow_pickle= True ) X_train = X_train.astype( "float" ) / 255 X_test = X_test.astype( "float" ) / 255 y_train = np_utils.to_categorical(y_train, num_classes) y_test = np_utils.to_categorical(y_test, num_classes) return X_train, y_train, X_test, y_test モデルを学習する関数 def train (X, y, X_test, y_test): model = Sequential() model.add(Conv2D( 32 ,( 3 , 3 ), padding= 'same' ,input_shape=X.shape[ 1 :])) model.add(Activation( 'relu' )) model.add(Conv2D( 32 ,( 3 , 3 ))) model.add(Activation( 'relu' )) model.add(MaxPooling2D(pool_size=( 2 , 2 ))) model.add(Dropout( 0.1 )) model.add(Conv2D( 64 ,( 3 , 3 ), padding= 'same' )) model.add(Activation( 'relu' )) model.add(Conv2D( 64 ,( 3 , 3 ))) model.add(Activation( 'relu' )) model.add(MaxPooling2D(pool_size=( 2 , 2 ))) model.add(Dropout( 0.25 )) model.add(Flatten()) model.add(Dense( 512 )) model.add(Activation( 'relu' )) model.add(Dropout( 0.45 )) model.add(Dense(num_classes)) model.add(Activation( 'softmax' )) opt = RMSprop(lr= 0.00005 , decay= 1e-6 ) model.compile(loss= 'categorical_crossentropy' ,optimizer=opt,metrics=[ 'accuracy' ]) model.fit(X, y, batch_size= 28 , epochs= 40 ) model.save( './cnn.h5' ) 動作の確認。作成したモデルをロードしテスト用の写真を判別できるかやってみます! import keras import numpy as np from PIL import Image from keras.models import load_model model = load_model( "./cnn.h5" ) img = Image.open( "./chacha_pic_for_test.jpg" ) img = img.convert( 'RGB' ) img = img.resize(( 64 , 64 )) img = np.asarray(img) img = img / 255.0 prd = model.predict(np.array([img])) prelabel = np.argmax(prd, axis= 1 ) if prelabel == 0 : print ( "チャチャの写真です" ) elif prelabel == 1 : print ( "クロの写真です" ) elif prelabel == 2 : print ( "どちらでもないです" ) 実行するタイミングを決める 餌やり器から餌がなくなった状態を検出し、そのタイミングから60秒さかのぼって上記のコードを実行するようにします。 def is_esa_empty (image_from_safie: bytes ) -> bool : img_binarystream = io.BytesIO(image_from_safie) img_pil = Image.open(img_binarystream) img_numpy = np.asarray(img_pil) img = cv2.cvtColor(img_numpy, cv2.IMREAD_COLOR) # 餌の皿部分以外をマスクする h, w = img.shape[: 2 ] mask = np.zeros((h, w), dtype=np.uint8) cv2.circle(mask, center=( 645 , 340 ), radius= 170 , color= 255 , thickness=- 1 ) img[mask== 0 ] = [ 255 , 255 , 255 ] # mask の値が 0 の画素は黒で塗りつぶす。 # 白黒に変更 img_bw = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二値化 _, thresh = cv2.threshold(img_bw, 106 , 255 , cv2.THRESH_BINARY_INV) # 輪郭検出 contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 輪郭描画 cv2.drawContours(img, contours, - 1 , ( 0 , 255 , 0 ), 2 ) # 餌ひとかけらの面積を400として餌の数を算出 esa_count = int ( sum ([cv2.contourArea(contour, False ) for contour in contours]) / 400 ) # 餌の数が10を切ったらemptyとする return True if esa_count < 10 else False いつでもどこでも見られるようにする 最後に、Safieのカメラと同じようにいつでもどこでも猫の餌の状況を確認できるように、上記のコードで取得できたデータをデータベースに保存し、Webサーバを立ててアクセスできるようにしました。 どちらの猫がいつどのくらい食べているのか簡単に分かり健康管理に役立てています。 また、在宅していないタイミングでも元気に餌を食べてるか確認できるのでとても便利です。 以上、SafieカメラとAPIを利用して、飼い猫の給仕DXをした話でした。皆さんも是非Safie APIを使って日々の生活や業務の効率化に役立ててみてください! 参考文献: AI Academy - Deep Learningで犬・猫を分類してみよう