TECH PLAY

セーフィー株式会社

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

221

こんにちは!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で犬・猫を分類してみよう
アバター
はじめに CVPR2024は、コンピュータビジョンとパターン認識の分野における最前線の研究成果を集める国際会議です。今年の論文提出数は11532件で、昨年のCVPR2023から26%の増加を記録しました。その中で採択されたのは2719件、採択率は23.6%です。この中から特に優れた24件の論文が Best Paper Award 候補として選出されました。 本記事では、これらのアワード候補となった論文の概要と、その技術的な特徴を紹介します。最先端の技術動向の理解や、今後の研究開発に役立てていただければ幸いです。 はじめに Best Paper Award 候補 Objects as volumes: A stochastic geometry view of opaque solids Repurposing Diffusion-Based Image Generators for Monocular Depth Estimation Comparing the Decision-Making Mechanisms by Transformers and CNNs via Explanation Methods MMMU: A Massive Multi-discipline Multimodal Understanding and Reasoning Benchmark for Expert AGI EventPS: Real-Time Photometric Stereo Using an Event Camera MemSAM: Taming Segment Anything Model for Echocardiography Video Segmentation Correlation-aware Coarse-to-fine MLPs for Deformable Medical Image Registration Producing and Leveraging Online Map Uncertainty in Trajectory Prediction SpiderMatch: 3D Shape Matching with Global Optimality and Geometric Consistency PaSCo: Urban 3D Panoptic Scene Completion with Uncertainty Awareness PlatoNeRF: 3D Reconstruction in Plato’s Cave via Single-View Two-Bounce Lidar Temporally Consistent Unbalanced Optimal Transport for Unsupervised Action Segmentation Rich Human Feedback for Text-to-Image Generation BIOCLIP: A Vision Foundation Model for the Tree of Life Grounding and Enhancing Grid-based Models for Neural Fields NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation Mip-Splatting: Alias-free 3D Gaussian Splatting pixelSplat: 3D Gaussian Splats from Image Pairs for Scalable Generalizable 3D Reconstruction MLP Can Be A Good Transformer Learner Task-Driven Wavelets using Constrained Empirical Risk Minimization Image Processing GNN: Breaking Rigidity in Super-Resolution Generative Image Dynamics Analyzing and Improving the Training Dynamics of Diffusion Models EGTR: Extracting Graph from Transformer for Scene Graph Generation おわりに Best Paper Award 候補 Award Candidate 論文の概要をセッション順に記載します。 Objects as volumes: A stochastic geometry view of opaque solids 著者:Bailey Miller · Hanyu Chen · Alice Lai · Ioannis Gkioulekas セッション:Orals 1B: Vision and Graphics 著者らは、物理法則に基づいて不透明物体をボリュームモデルで表現する理論を提案しています。まず、ランダムな指示関数を用いて不透明物体を確率的に表現し、光が物体を通過する際に減衰が指数関数的に変化する条件から、光の減衰係数を定式化します。この理論の一般化することで、等方性および異方性の散乱や、不透明物体の陰関数表現に対応できることを示しています。この手法によって、優れた3次元再構成を行うことができます。 本論文で提案している理論の概要を示しています。減衰係数 を密度(density)と投影面積(projected area)の積として表します。密度は、平均された陰関数(mean implicit)から空隙率(vacancy)を通して計算することができます。また、投影面積は、異方性パラメータを用いて計算します。 Repurposing Diffusion-Based Image Generators for Monocular Depth Estimation 著者:Bingxin Ke · Anton Obukhov · Shengyu Huang · Nando Metzger · Rodrigo Caye Daudt · Konrad Schindler セッション:Orals 3A: 3D from Single View 単眼深度推定は一般に、学習データに未知のデータに対して難易度が高い傾向にあります。そこで著者らは、豊富な事前知識をもつ既存の生成拡散モデルを利用した単眼深度推定手法を提案しています。提案法は Stable Diffusion から派生した手法で、合成データ(synthetic data)でファインチューニングされています。またゼロショット転移を用いることで未知のデータにも強く、単眼深度推定タスクでSOTAな結果を出しています。 ファインチューニングの過程は以下のステップで行われます。事前訓練されたStable Diffusionを利用し、画像 と深度 を潜在空間 と に変換します。 にノイズを付加し後、 と連結し、これらを入力に元の深度の潜在空間を復元するようにU-Netをファインチューニングします。損失関数は、元のノイズ とモデルが予測したノイズ の間の二乗誤差となっています。 Comparing the Decision-Making Mechanisms by Transformers and CNNs via Explanation Methods 著者:Mingqi Jiang · Saeed Khorram · Li Fuxin セッション: Orals 3B: Vision, Language, and Reasoning 著者らは、視覚認識バックボーンモデルの意思決定を分析するために、サブ説明カウント法(sub-explanation counting) と クロステスティング(cross-testing)の2つの方法を提案しています。これにより、ネットワークが持つ合成性(compositionality)と分離性(disjunctivism)という2つの特性の違いが明らかになります。 収穫機の画像をサブ説明カウント法で分析しています。合成的なモデルであるConvNeXtやトランスフォーマーは、画像の複数の部分を一緒に見て意思決定を行います。したがって、右上のツリーのように入力画像の一部がマスクされた場合、推論されるクラスラベルの信頼度はわずかに減少します。一方で、分離的なモデルであるCNNや蒸留トランスフォーマーは、少ない部分から判断するため、右下の画像のように大きな信頼度を出力します。この分析は、各モデルがどのように画像の部分を利用しているかを明らかにします。 MMMU: A Massive Multi-discipline Multimodal Understanding and Reasoning Benchmark for Expert AGI 著者:Xiang Yue · Yuansheng Ni · Kai Zhang · Tianyu Zheng · Ruoqi Liu · Ge Zhang · Samuel Stevens · Dongfu Jiang · Weiming Ren · Yuxuan Sun · Cong Wei · Botao Yu · Ruibin Yuan · Renliang Sun · Ming Yin · Boyuan Zheng · Zhenzhu Yang · Yibo Liu · Wenhao Huang · Huan Sun · Yu Su · Wenhu Chen セッション:Orals 3B: Vision, Language, and Reasoning 著者らは、マルチモーダルなモデルを評価するための、大学レベルの知識と熟考を要する大規模な複数分野のタスクからなる新しいベンチマークを提案しています。MMMUは、既存のベンチマークとは異なり特定分野の知識を必要とする高度な認識と推論に焦点を当てており、14のオープンソースLLMや、GPT-4V (ision)、Geminiの評価結果からも、その難しさが示されています。MMMUは、次世代の専門的な人工知能モデルの構築を促進することを期待しています。 MMMUには大学の試験、クイズ、教科書から厳選された11,500の多様な質問が含まれており、アート&デザイン、ビジネス、科学、健康&医療、人文社会科学、技術&工学の6つの主要分野をカバーしています。チャート、図、地図、表、楽譜、化学構造など、異種性の高い30種類の画像も含まれています。MMMUのテストでは、GPT-4VとGemini Ultraがそれぞれ56%と59%の精度しか達成しておらず、これらのモデルにはまだ改良の余地が大きいことを示しています。 EventPS: Real-Time Photometric Stereo Using an Event Camera 著者:Bohan Yu · Jieji Ren · Jin Han · Feishi Wang · Jinxiu Liang · Boxin Shi セッション:Orals 3C: Medical and Physics-Based Vision 本記事を執筆している 2024/6/4 時点で論文が未公開のため、関連情報を記載します。 タイトルから、本提案はイベントカメラを用いてリアルタイムにPhotometric Stereo(照度差ステレオ)を行う手法と考えられます。イベントカメラとはマイクロ秒単位の時間分解能を持つカメラデバイスです。Photometric Stereo は照明方向を変えながら撮影を行い、画素ごとの明るさの変化から表面の法線方向を推定する手法です。 著者らが公開しているデモ動画では、イベントカメラの撮影結果、法線の推定結果、測定するオブジェクトなどが示されています。 MemSAM: Taming Segment Anything Model for Echocardiography Video Segmentation 著者:Xiaolong Deng · Huisi Wu · Runhao Zeng · Jing Qin セッション:Orals 3C: Medical and Physics-Based Vision 著者らは、SAM(Segment Anything Model)を医療ビデオに適用することで、超音波ビデオセグメンテーションの課題に取り組んでいます。提案モデルは、空間と時間情報を組み合わせた時空メモリの使用と、予測されたマスクを活用してメモリの品質を向上させるメモリ強化メカニズムを提案しています。これにより、セグメンテーションの精度と一貫性が向上しています。 提案法は主にSAMとメモリの2つのコンポーネントで構成されています。画像エンコーダは入力画像を、プロンプトエンコーダは外部プロンプトをそれぞれ埋め込みにエンコードし、マスクデコーダが統合してセグメンテーションマスクを予測します。画像埋め込みはメモリ特徴空間に保存され、デコード時にメモリからメモリプロンプトが読み出され利用されます。メモリは強化とエンコーダを通じて更新されます。 Correlation-aware Coarse-to-fine MLPs for Deformable Medical Image Registration 著者:Mingyuan Meng · Dagan Feng · Lei Bi · Jinman Kim セッション:Orals 3C: Medical and Physics-Based Vision 近年の医用画像位置合わせでは、変形の大きい脳や心臓のMRI画像に対応するため、Transformer ベースの手法が注目されていますが、計算コストが高く高解像度の特徴を扱えない課題があります。著者らは、MLP(多層パーセプトロン)ベースのネットワークを提案しています。提案法は、複数の解像度の特徴マップを利用して、粗から密(coarse-to-file)へ段階的に位置合わせ(registration)を行います。この手法は、高い計算効率を保ちながら、変形の大きい医用画像に対してもリアルタイムに優れた性能を発揮します。 提案法は、CNNベースの階層的な特徴抽出エンコーダと、CMW-MLPブロックを用いたデコーダから構成されます。初期ステップでは、エンコーダから得られた最も粗い解像度の特徴マップ( と )を使用し、初期の変形フィールド を生成します。以降のステップでは、前のステップで得られた変形フィールドを使用して、次の解像度の特徴マップを変形し、再度CMW-MLPブロックに入力します。このプロセスを繰り返すことで、段階的に詳細な位置合わせを実現します。 Producing and Leveraging Online Map Uncertainty in Trajectory Prediction 著者:Xunjiang Gu · Guanyu Song · Igor Gilitschenski · Marco Pavone · Boris Ivanovic セッション:Orals 4A: Autonomous Navigation and Egocentric Vision 著者らは、Autonomous Vehicle(AV)の軌道予測において、オンラインマップの不確実性を推定する手法を提案しています。提案法は、不確実性を考慮することで軌道予測の学習の収束が最大で50%速くなり、軌道予測性能が最大で15%向上します。 AVで取得したカメラやLiDARなどのセンサーデータは、BEV(Bird’s Eye View)特徴量にエンコードされます。特徴量を用いて、分類モデルはマップ要素(車線、道路境界など)を予想します。回帰モデルは、ポリラインやポリゴンなどのマップ要素の頂点と、ラプラス分布で表される不確実性を予測します。生成された確率的なオンラインマップは、GNN(Graph Neural Network)やTransformer などの確率的エンコーダに入力され、軌道予測が行われます。 SpiderMatch: 3D Shape Matching with Global Optimality and Geometric Consistency 著者:Paul Roetzer · Florian Bernard セッション:Orals 4B: 3D Vision 著者らは3次元形状マッチングのための新しい経路ベースの手法を提案しています。様々な種類のマッチング問題を解くための一般的なアプローチとして、直積空間上での最短経路を求める方法があります。これは多項式時間で大域的に最適な解を計算することができますが、3次元形状マッチングへの自然な一般化は難しいことが広く知られています。著者らはこれらの課題を解決し、効率的に大域最適な解を得る手法を提案しています。 提案法は、3次元形状を3次元形状表面をなぞる長い自己交差曲線(SpiderCurve)として表現します。その結果、密なジオメトリに対しても、幾何的に矛盾のない形状マッチングができることを示しています。実行時間の検証では、メッシュ数が増えても現実的な時間でスケールしています。 PaSCo: Urban 3D Panoptic Scene Completion with Uncertainty Awareness 著者:Anh-Quan Cao · Angela Dai · Raoul de Charette セッション:Orals 4B: 3D Vision 著者らは、不完全な3次元点群からジオメトリ、セマンティック、インスタンスを推定するPanoptic Scene Completion (PSC)という新しいタスクを提案しています。提案手法はマルチスケールスパース生成デコーダーを利用し、さらにMulti-Input Multi-Outputを導入することで、不確実度を推定できるようにしています。 提案するネットワークは、同一のアーキテクチャを持つ複数のサブネットが含まれます。各サブネットは一部のパラメータを除いて共通のパラメータを使用します。1つの点群データからデータ拡張を行い、複数の入力データを生成し、それぞれの入力データをサブネットで演算することで、入力データと同数のPSCを出力します。複数の予測をアンサンブルすることで、不確実性の推定と予測精度の向上を行います。 PlatoNeRF: 3D Reconstruction in Plato’s Cave via Single-View Two-Bounce Lidar 著者:Tzofi Klinghoffer · Xiaoyu Xiang · Siddharth Somasundaram · Yuchen Fan · Christian Richardt · Ramesh Raskar · Rakesh Ranjan セッション:Orals 4B: 3D Vision 著者らは、単一視点で3D再構築を行う手法を提案しています。従来の単一視点の3D再構築に使われていたNeRF(Neural Radiance Fields)は、物理的に正確ではない事前情報に依存しています。本手法は、LiDAR(Light Detection and Ranging)データとNeRFを用いて、複雑なシーンの3Dモデルを生成します。本手法は、2回までの反射光を測定する技術を活用し、従来のNeRF手法と比較して、環境光や反射光の事前データに依存せずに再構築精度を向上させています。 シーンの各点をパルスレーザーで照射し、反射した光の飛行時間を測定します。このデータを用いてNeRFを訓練し、一次反射および二次反射光の経路をモデリングします。生成される3Dジオメトリは、密度推定に基づいて可視部分と隠された部分の両方を高精度に再構築します。 Temporally Consistent Unbalanced Optimal Transport for Unsupervised Action Segmentation 著者:Ming Xu · Stephen Gould セッション:Orals 4C: Action and Motion 著者らは、長時間動画のアクションセグメンテーションタスクに対して、ASOT(Action Segmentation Optimal Transport)という手法を提案しています。この手法は、従来の方法が抱えるアクション順序の仮定が必要であることや、アクションの時間的一貫性の欠如といった課題を解決しています。また、GPUを用いた数値解法アルゴリズムも提案されています。 ビデオフレームとアクションの間のコスト行列 には大きなノイズが含まれています。提案法(ASOT)は、アクションカテゴリが変更されるコスト と、隣接するビデオフレームでアクションが変更されるコスト を加味することで、最適な割り当て を予測します。提案法は最適輸送問題の一種である、GW(Gromov-Wasserstein)問題に基づいて構築されています。 Rich Human Feedback for Text-to-Image Generation 著者:Youwei Liang · Junfeng He · Gang Li · Peizhao Li · Arseniy Klimovskiy · Nicholas Carolan · Jiao Sun · Jordi Pont-Tuset · Sarah Young · Feng Yang · Junjie Ke · Krishnamurthy Dvijotham · Katherine Collins · Yiwen Luo · Yang Li · Kai Kohlhoff · Deepak Ramachandran · Vidhya Navalpakkam セッション:Orals 5A: Datasets and Evaluation 最近のText-to-Imageモデルは進歩していますが、生成画像には不自然さ、テキストとの不一致などの問題があります。これに対し、本研究では、生成画像の問題のある領域を詳細にアノテーションし、18Kの人間のフィードバックデータ(RichHF-18K)を収集しました。このデータセットから、フィードバックを予測するマルチモーダル変換器を訓練することで、ファインチューニングに使用するための高品質な画像を選別したり、生成画像中の問題のある領域のマスクを作成することに活用できます。 フィードバックモデルのアーキテクチャは、Text-to-Imageモデルからの生成画像とそのテキストプロンプトを入力とします。ViT(ビジョントランスフォーマー)から出力される画像トークンと、Text-embedモジュールから出力されるテキストトークンに自己注意機構を適用し、画像とテキスト情報を融合します。画像トークンは特徴マップに再構成され、不自然な箇所のヒートマップと品質スコアを出力します。テキストトークンは、画像トークンと共にトランスフォーマーデコーダに送られ、入力されたプロンプトと生成結果のどこがずれていたかを説明する文章を出力します。 BIOCLIP: A Vision Foundation Model for the Tree of Life 著者:Samuel Stevens · Jiaman Wu · Matthew Thompson · Elizabeth Campolongo · Chan Hee Song · David Carlyn · Li Dong · Wasila Dahdul · Charles Stewart · Tanya Berger-Wolf · Wei-Lun Chao · Yu Su セッション:Orals 5A: Datasets and Evaluation 著者らは、生物の画像から特徴量を抽出するための大規模なマルチモーダルモデルBioCLIPを提案しています。「TreeOfLife-10M」と呼ばれる45万種以上の生物を含む1000万枚以上の画像データセットを構築し、そのデータを用いてCLIPスタイルのモデルを学習しています。提案法により、10種類のデータセットに対する5ショット分類の平均精度が、51.5%(従来法:CLIP)から 68.8%に向上しました。 提案法は、テキストと画像の対照学習を行います。テキストをエンコードする際に、着目する階層よりも上位の分類名を含めることで、生物分類の階層の特徴と、画像の特徴の関連を学習します。対照学習では、同種のテキストの埋め込みと画像の埋め込みは近くなるように、異種のペアは遠くなるような目的関数を用います。これにより、提案モデルに系統樹の階層構造が反映されます。 Grounding and Enhancing Grid-based Models for Neural Fields 著者:Zelin Zhao · FENGLEI FAN · Wenlong Liao · Junchi Yan セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、ニューラルフィールドの表現に一般的に利用されるグリッドベースのモデルを分析するための理論的枠組みを提案します。これらのモデルの近似および汎化挙動は、Grid Tangent Kernelsと呼ばれるグリッドモデル固有の量よって決定されることを示します。この枠組みを用いて、MulFAGridという新しいモデルを提案し、2D画像フィッティング、3D符号付き距離場(SDF)の再構築などのタスクで優れた性能が得られることを示しました。 上側の処理経路は、座標 をインデックス関数に入力し、位置に対してノードの集合を返し、ノード に関連付けられた特徴(重み)ベクトルを計算します。下側の経路は、多重化フィルターを用いてフーリエ特徴量を処理した後、正規化レイヤーを用いてカーネル関数を計算します。最後に、特徴ベクトルとカーネル関数を要素ごとに乗算して求める関数の近似値を出力します。 NeRF-HuGS: Improved Neural Radiance Fields in Non-static Scenes Using Heuristics-Guided Segmentation 著者:Jiahao Chen · Yipeng Qin · Lingjie Liu · Jiangbo Lu · Guanbin Li セッション:Orals 5B: 3D from Multiview and Sensors NeRFを用いて3Dモデルを構築する際、動的なシーンでは物体の移動や影の変化により画像間の一貫性が失われ、適用が難しいという問題がありました。この問題を解決するために、ヒューリスティックを用いて動的シーンから静的な3Dモデルを構築する手法を提案しています。合成データと実データを用いた実験により、提案手法は従来手法を上回る性能を示しています。 最下部に示されている提案法は2種類のヒューリスティックを用います。SfM(Structure-from-Motion)ヒューリスティックは、静的な物体において特徴点のマッチング頻度が高いことを利用します。カラー残差ヒューリスティックは、NeRFモデルのカラー残差が静的な物体で低いことを利用します。これらの情報をプロンプトとしてSAM(Segment Anything Model)に入力することで正確な静的マップが得られます。 Mip-Splatting: Alias-free 3D Gaussian Splatting 著者:Zehao Yu, Anpei Chen, Binbin Huang, Torsten Sattler, Andreas Geiger セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、3D Gaussian Splatting(3DGS)技術において、様々なスケールで高品質に物体の3D画像をレンダリングするMip-Splattingという手法を提案しています。従来法は、ズームインズームアウトを行う際にノイズや歪みなどのエイリアスや、不自然なエッジのようなアーティファクトが生じる問題がありました。提案法では、高周波成分の処理やレンダリング時のフィルタの変更によってこれらの問題を解決しています。 3DGSは3Dシーンを3D ガウシアンで表現し、フィルタ処理を行い画像をレンダリングします。標本化定理に基づく適切なレンダリングのサンプリングレートを選択しない場合、高周波成分が正しく処理されず、エイリアスが発生します。また、焦点距離やカメラ距離の変化によって3D ガウシアンのサイズが過度に大きくなる時には、ぼやけや歪みが発生します。提案法では、3D smoothing filterを用いて3Dガウシアンのサイズを制約することで、高周波成分に含まれるアーティファクトを抑制します。また、レンダリング時に適切なフィルタサイズを選択することでエイリアスを抑制します。 pixelSplat: 3D Gaussian Splats from Image Pairs for Scalable Generalizable 3D Reconstruction 著者:David Charatan · Sizhe Lester Li · Andrea Tagliasacchi · Vincent Sitzmann セッション:Orals 5B: 3D from Multiview and Sensors 著者らは、画像のペア群から3次元輝度場を再構成するフィードフォワードモデルを提案しています。提案法は、3次元輝度場の高速な推論と、リアルタイムでメモリ効率の良いレンダリングを特徴としています。実世界のRealEstate10kデータセットとACIDデータセットにおいて、解釈可能で編集可能な3次元輝度場を再構成しながら、最先端のライトフィールド変換器を凌駕し、レンダリングを2.5倍高速化します。 提案モデルは3次元輝度場を3次元ガウスプリミティブによってパラメータ化します。入力画像の画素の特徴量 ( はピクセル座標) から、ガウスプリミティブのパラメータ を生成し、ガウススプラッティングによりレンダリングすることで2次元画像を求めます。 は入力画像をResNet-50やDINO ViT-B/8でエンコードしたものです。ガウスプリミティブのパラメータは が平均、 が共分散、 が透明度、 が球面調和関数の係数です。 と はNNモデル により予測され、 と は が予測したピクセルごとの深さの離散確率分布から間接的に予測されます。 MLP Can Be A Good Transformer Learner 著者:Sihao Lin · Pumeng Lyu · Dongrui Liu · Tao Tang · Xiaodan Liang · Andy Song · Xiaojun Chang セッション:Orals 5C: Low-Shot, Self-Supervised, SemiSupervised Learning セルフアテンションはトランスフォーマーの重要な要素ですが、計算リソースを多く必要とすることが課題です。著者らは、この問題を改善する手法を提案しています。具体的には、エントロピーを利用して重要性の低いアテンション層を特定し、レイヤーを軽量化することでメモリーの負荷を削減します。また、重要性の低いアテンション層の知識をその後段のMLP(Multi-Layer Perceptron)層に移すための学習手法も提案しています。 上の図は、DeiT-Bの各ブロックのエントロピーを測定し、入力に近い層ではアテンション層とそれに続くMLPレイヤでは同程度にエントロピーが低いことを示しています。次に、それを恒等写像(Identical Mapping)に置き換えることでレイヤーの修正を行います。結果としてDeiT-Bのパラメタを13.7%削減し、同一メモリ量で処理できる画像の量が20.5%増加しています。 Task-Driven Wavelets using Constrained Empirical Risk Minimization 著者:Eric Marcus · Ray Sheombarsing · Jan-Jakob Sonke · Jonas Teuwen セッション:Orals 6A: Low-level Vision and Remote Sensing 従来のディープニューラルネットワークでは、損失関数を追加することでソフト制約を課すことが一般的です。著者らは一部のパラメータに厳密な制約を設ける新しいフレームワークCERMを提案しています。具体的には、ニューラルネットワークの重みに特定の制約を課すことで、モデルの特性を制御します。これにより、異なるスケールの情報やノイズの影響を効率的に低減し、医療画像のタスクで優れた性能を示しています。 提案法は、「制約付き経験リスク最小化」(CERM)を用いて、畳み込みフィルターをウェーブレットに制約します。まず、ウェーブレットを特徴付ける方程式を導出します。導出された方程式から、リファインメントマスクと呼ばれるウェーブレットを一意に定める多項式を得ます。畳み込みフィルターに対して、リファインメントマスクの係数を制約として課します。これらの制約の下で、ネットワークを学習します。提案法を用いることで、特定のパターンを効果的に捉えることができます。 Image Processing GNN: Breaking Rigidity in Super-Resolution 著者:Yuchuan Tian · Hanting Chen · Chao Xu · Yunhe Wang セッション:Orals 6A: Low-level Vision and Remote Sensing 著者らは、超解像(Super-Resolution: SR)タスクにおいて、Graph Neural Network(GNN)を利用した手法(Image Processing GNN: IPG)を提案しています。提案法では、画素をノードとする2種類のグラフを構築します。ローカルグラフは、近隣の画素でノードを接続することで細部のディテールやテクスチャの復元を行います。一方、グローバルグラフは、画像全体で画素を接続することで、大局的なパターンを捉えます。また、提案法は、画像の単純で均一な部分と、細かいディテールが含まれている部分で、ノード間の接続数を調整し、効率的な情報集約を行います。 Urban100データセットを用いた検証結果では、提案手法(IPG)は、Bicubic補間や他の超解像手法よりも、元の高解像度画像(HQ)をより正確に復元しています。 Generative Image Dynamics 著者:Zhengqi Li · Richard Tucker · Noah Snavely · Aleksander Holynski セッション:Orals 6B: Image & Video Synthesis 著者らは、静止画像から自然でリアルな動きを持つ映像を生成する Generative Image Dynamics という手法を提案しています。提案法は、木々や花などの自然な動きを含む動画を教師データとして拡散モデルを学習します。この手法により、静止画像がインタラクティブで魅力的なコンテンツに変換されることが期待されます。 静止画像とノイズのある潜在変数をデノイズネットワーク に入力すると、各画素の動きを周波数領域で表現したスペクトルボリュームを予測します。 は2D 空間層とアテンション層が交互に配置されており、これにより特徴量マップ内のノイズを減少させながら、空間的な関係と周波数間の関係を統合します。スペクトルボリュームを逆フーリエ変換することで、各画素の変位を求めることができます。 Analyzing and Improving the Training Dynamics of Diffusion Models 著者:Tero Karras · Miika Aittala · Jaakko Lehtinen · Janne Hellsten · Timo Aila · Samuli Laine セッション:Orals 6B: Image & Video Synthesis 著者らは、一般的なADM (Ablated Diffusion Model) アーキテクチャにおいて、高レベルの構造を変更せずに拡散モデルの学習時に問題となる確率的な挙動をする損失関数等に対処するため、ネットワーク層を再設計しています。具体的には、学習過程でアクティベーション出力および重みの大きさを一定の範囲内に維持するようにしています。これにより、ImageNet-512の合成において、生成画像と実画像の統計的類似度を評価する指標であるFIDが2.41から1.81に改善されています。 U-NetベースのADMでは、エンコーダはスキップ接続を用いてデコーダに接続され、埋め込みがノイズレベルとクラスラベルを条件付けます。ADMはResNetをベースとしていますが、メインパスでは正規化が無いため、アクティベーション出力の増加を抑えられません。提案手法では、メインパスでのアクティベーション出力を一定の範囲に維持するために、MPブロック等を導入します。 EGTR: Extracting Graph from Transformer for Scene Graph Generation 著者:Jinbae Im · JeongYeon Nam · Nokyung Park · Hyungmin Lee · Seunghyun Park セッション:Orals 6C: Multi-Modal Learning シーングラフ生成(Scene Graph Generation: SGG)は、画像内のオブジェクトとそれらの間の関係を構造化するタスクです。SGGの従来法の多くはオブジェクト検出と関係予測を別々のステージで行うためモデル構造が複雑になり、計算コストが高くなることや、一貫性のある学習が難しいといった課題があります。提案法ではセルフアテンションを用いた軽量なワンステージモデルでこれらの問題を克服しています。 提案法は、入力画像をDETRエンコーダでエンコードし、続いてDETRデコーダーでオブジェクトを検出します。同時に、DETRデコーダーのアテンション層ごとの特徴量を用いることで、オブジェクト同士の関係性表現(Relation Representation) を計算します。関係性表現は、アテンション層ごとの重みを用いて、加重和がなされます。Relation Headは、関係性のラベルやその強度を推論し関係性グラフ(Relation Graph)を計算します。最後に検出したオブジェクトと関係性グラフを組み合わせてシーングラフを作成します。 おわりに 本記事では、CVPR2024のBest Paper Award候補となった論文を紹介し、コンピュータビジョンとパターン認識の分野における重要な進展を示しました。いずれの論文も興味深く、面白い内容でした。 現在、私の所属するAI Visionグループは9人で構成されています。今回の論文紹介は、各メンバーが分担して取り組みました。私たちのグループでは、最先端の研究を取り入れたAIの開発を行っています。興味のある方は募集要項をご覧ください。
アバター
はじめに 第 2 開発部モバイルグループで iOS テックリードをしている鞆です。 2024 年 5 月 23 日に開催された Findy さん主催のイベント TechBrew in 東京 ~モバイルアプリの技術的負債に向き合う~ にて、発表させていただきました。 findy.connpass.com 当日の様子を含めてイベントレポート的な形でご紹介できればと思います! はじめに 会場 オープニング 発表内容 Bitkeyのモバイルアプリを進化させるための歩き方 @arasan01_me モバイルアプリの技術的負債に全社を挙げて取り組む考え方 @mikity01985 パッケージ管理でモバイル開発を安全に進める @entaku_0818 GitHub Copilotで技術的負債に挑んでみる 懇親会 会場 Findy さんが 2024 年 4 月に大崎駅近くにオフィス移転されたとのことで、この新オフィスに併設されたイベントスペースが会場でした。 findy.co.jp 大崎駅からのアクセス についても画像付きで紹介されており、イベントスペース自体もオフィス受付のすぐ横にあるため、迷うことはなさそうです。 イベントスペース自体も 100 名収容可能ということで余裕があり、クロークやコントロールルームまである非常に本格的な作りでした。 開催時間が遅めということもあってか照明についても暗めとなっており、非常に雰囲気のある状態に仕上がっていました。 受付向かって右側すぐがイベントスペース入り口 スクリーン横には音響管理用のコントロールルーム 今回のイベントでは利用しませんでしたがカウンタースペースもありました オープニング Findy さんのイベントではオープニングで必ず乾杯!から始まるとのことなので、まずは全員で乾杯してイベントの開始となりました。 またここで、 Findy さんが作ったという IPA (クラフトビールのほうです) の紹介もありました。 挑戦 IPA 開発生産性 IPA Findy さん主催のイベントで付いてくる「TechBrew」という名称が正直なところ謎だったのですが、文字通り本当に醸していました。 挑戦 IPA と開発生産性 IPA の 2 種類があり、オフラインイベントで振舞われているとのことでしたが、限定本数となるため気になる方はお早めにとのことでした! 発表内容 私のほうからは、セーフィーで開発している Safie Viewer の iOS 版アプリ に関する技術的負債への取り組みに関して、ここ 1 年ほどで実施、検討している内容について事例紹介といった形で発表させていただきました。 speakerdeck.com 10 分間の発表に詰め込みすぎてしまった感があるのが反省です。 また、今回のテーマである「モバイルアプリの技術的負債に向き合う」についても、セーフィーのモバイルグループが置かれている現状にまさにマッチしたものとなっており、発表された内容それぞれが非常にためになりました。 Bitkeyのモバイルアプリを進化させるための歩き方 @arasan01_me speakerdeck.com Bitkey さんの workhub アプリに関して、専業エンジニアがいなかった React Native 時代から現在までの歴史に関して、技術選定の方針やネイティブ化への移行対応等の具体例を交えた発表でした。 TCA の採用についても一度見送ってから再度検討して採用する等、外部環境の変化に対して素早く対応していけるというところに開発チームの強さを感じました。 「技術選定の正解は?」という問に対して、「自分たち (開発チーム) がいいと思う方法全てが正解」という部分が非常に印象的で、セーフィーのモバイルグループでも技術選定で迷うことが非常に多いのですが、これに対して勇気づけられるような発表でした。 モバイルアプリの技術的負債に全社を挙げて取り組む考え方 @mikity01985 speakerdeck.com モバイルアプリの技術的負債を組織内でどう扱っていくかという点に関して、 EM ならではの高い視座で俯瞰的に見ていくような発表で、セーフィーの現状にも当てはまる部分が多かったです。 特に「技術的負債の中でもチームに閉じてしまうものは外部への説明に困る」といった指摘は過去を振り返ってみてもまさにその通りで、外部とのコミュニケーションに関する方法論等は非常に参考になりました。 モバイルアプリ開発における Four Key Metrics の難しさ等についても腹落ちする部分が多く、個人的には共感できる部分が多い発表でした。 パッケージ管理でモバイル開発を安全に進める @entaku_0818 speakerdeck.com 比較的歴史のある Voicy さんのアプリにおける過度な共通化という問題とそれに対する回答としての責務分割のお話しでした。 Swift Package Manager を利用してモジュール / パッケージとしてカプセル化することによって、責務としてのインターフェイスを明示化、また依存性を制限することでシステムとして安全な状態を保っているとのことで、昨今流行りの Swift Package Manager ベースのマルチモジュール化の利点として説得力のある内容だったかと思います。。 Safie Viewer の iOS 版でもこの辺りはやろうと思っていて進んでいない部分だったので、先行事例として非常に参考になる発表でした。 GitHub Copilotで技術的負債に挑んでみる www.docswell.com 意外と貴重なモバイル開発領域における GitHub Copilot 利用に関する発表でした。 特に iOS 開発ですと、 Xcode が公式に対応しておらず 3rd party ツールに依存してしまうということもあり、セーフィー社内や個人的にも GitHub Copilot を有効活用しきれていないという感触はあったのですが、やはり同様の辛みがあるのだなという納得感がありました。 また実際にデモで見せていただいた GitHub Copilot Chat との連携についてはほぼ利用していなかったため、 code suggestion 以外の使い方として一つ知見が増えました。 懇親会 一通り発表が終了した後は立食形式での懇親会となりました。 IPA も十分に用意いただいてました! 完全オフラインイベントということもあり、比較的小規模な集まりとはなったのですが、逆に参加者間でコミュニケーションが取りやすいといった面がありました。 モバイルアプリに関するイベントだったのですが、他職種のエンジニアの方も参加されており、意外なところでネットワークの広がりもできました! まとめ 今回、 Findy さん側からお話しをいただいての発表、イベント参加だったのですがテーマ的にも得ることが多いイベントでした。 完全オフラインイベントへの参加というのもコロナ禍以降で久しぶりだったのですが、やはりオフラインならではの良さ、密度の高さというのを感じました。 最後になりますがイベント企画、ならびに会場を提供いただいた Findy さん、誠にありがとうございました!
アバター
はじめに Data-centric AI Cleanlab 物体検出データセットのクレンジング データセット作成 物体検出モデルの学習 予測と正解データの生成 Cleanlabによる品質スコア算出 アノテーション修正 Cleanlabをフル活用するためのTips 同じ正解データや予測を複数回マッチングさせないようにする 閾値をモデルやデータセットに応じて適切に設定する スコアが1.0になっている画像でも一通り確認する まとめ はじめに セーフィー株式会社  で画像認識AIの開発エンジニアをしている水野です。 現在、 AI-App 人数カウント で利用される物体検出モデルの精度改善に取り組んでいます。物体検出モデルの精度改善方法としては様々な手法が考えられますが、近年はData-centric AIというアプローチが注目されています。そこで本稿では、Data-centric AIで物体検出モデルの精度を改善する一手法として Cleanlab を用いたデータセットのクレンジング方法について紹介します。 Data-centric AI Data-centric AIとは、2021年3月に Andrew Ng氏の講演「From Model-centric to Data-centric AI」 で提唱された概念で、AIモデルを固定してデータセットを改善することでモデルの精度改善を実現するアプローチのことです。これは従来主流であったデータセットを固定してAIモデルを改善するModel-centric AIとは対極にある考え方と言えそうです。ただしこれらはどちらのアプローチが優れているという話ではなく、実際の開発では両方の観点で改善していく必要があります。 データセットを改善したい場合、どのようなアプローチが考えられるでしょうか。単純に考えると画像およびアノテーションを人の目で確認し、問題のあるデータを抽出して修正するという方法が挙げられそうですが、この方法にはいくつかの課題があります。 現実のデータセットでは画像枚数が数千枚~数万枚という規模であり、人の目で一通り確認しミス無く修正しきるのはかなり時間が掛かる データセットがクリーンであることを客観的に示すことが難しい このようなデータセット改善の作業で発生する課題を解決するために Cleanlab のようなData-centric AI向けのライブラリが活用できます。 Cleanlab Cleanlab は confident learning の考え方をベースとしてデータセットのノイズを抽出するためのライブラリです。Cleanlabを使用することでデータセット内に存在するアノテーションの問題を自動的に抽出することができます。また画像ごとの品質スコアが出力されるので、アノテーションの品質がどの程度なのか定量的に示すことが可能です。従来は画像識別タスクのみサポートされていたのですが、 2023年9月のリリース から物体検出タスクに対応しています。 Cleanlabを用いた基本的なデータクレンジングの流れは下記の通りです。 データセット作成: データセットをtrainとvalidationに分けます AIモデルの学習: trainデータでモデルを作成します 予測生成: 学習したモデルを用いてvalidationデータに対する予測を生成します Cleanlabによる品質スコア算出: validationデータに対する予測と正解データを比較して画像ごとの品質スコアを算出します アノテーション修正: 品質スコアの低い画像のアノテーションを修正します お気づきの方もいるかもしれませんが、このままではvalidationデータに含まれる画像の品質しか判定できません。そこで実際にはK-fold分割を用いたOut-of-fold予測を生成し、全データに対する品質スコアを算出します。5-fold分割時の処理を図で書くと以下のようになります。5つに分割したデータで生成した各モデルで予測を生成し、結合することで実質的に全データに対する予測を生成することが出来ます。本稿では簡単化のために技術的な背景や詳細の説明は省略しますが、ご興味のある方は 公式の解説ページ をご参照ください。 5-fold分割によるOut-of-fold予測生成の流れ ( 公式の解説ページ の図をベースに作成) 物体検出データセットのクレンジング ここからは物体検出データセットに対する実際のクレンジングの方法を説明します。 物体検出モデルとして YOLOv8 、データセットとして COCO を使用します。 データセット作成 クレンジングしたいデータセットをtrainとvalidationに分割します。既に説明した通り、データセット全体をクレンジングする場合はOut-of-fold予測を生成する必要がありますので、COCOデータセット全体をクレンジングしたい場合はCOCOから提供されているtrainデータとvalidationデータを混ぜた上でK-fold分割を実施します。 K-fold分割では scikit-learn がよく利用されます。データセットの特性に応じて適切な分割方法を選択する必要がありますので、 公式ページ を参照し各自の状況に合わせた手法を選択してください。また分割数をいくつにすべきかという問題ですが、理想的には多い方が良いのですが分割数が増えると学習に必要な時間が膨大になってしまうため、実用上は5-fold分割がお勧めです。 Kaggle のコンペ等でも5-fold分割はよく利用されますし、trainとvalidationの割合としても4:1というのはバランスが良いと思います。 5-fold分割したデータセットの構成例を以下に示します。YOLO形式のラベルフォーマット等は 公式の解説ページ をご参照ください。またCOCOのフルデータセットの場合、5-fold合計で100GB(1foldあたり20GB)のディスク容量を必要としますのでディスクの空き容量にご注意ください。 $ tree datasets/ datasets/ ├── coco_fold1 │   ├── images │   │   ├── train │   │   │   └── xxx.jpg │   │   └── val │   │   └── yyy.jpg │   └── labels │   ├── train │   │   └── xxx.txt │   └── val │   └── yyy.txt ├── coco_fold2 ├── coco_fold3 ├── coco_fold4 └── coco_fold5 各foldに対応するyamlファイルの作成も必要です。fold1の例を以下に示します。 # Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] path : ../datasets/coco_fold1 # dataset root dir train : images/train # train images (relative to 'path') val : images/val # val images (relative to 'path') test : # test images (optional) # Classes (80 COCO classes) names : 0 : person 1 : bicycle 2 : car # ... 77 : teddy bear 78 : hair drier 79 : toothbrush 物体検出モデルの学習 K-fold分割で生成した各foldで学習を実行しfold数分のモデルを生成します。まずは学習を実行するために公式のDockerコンテナを立ち上げます。 $ docker run --ipc=host -it --gpus all -v <DATASET_ROOT>:/usr/src/datasets ultralytics/ultralytics:latest 次にさきほど作成した各foldのデータセットごとに学習コマンドを実行します。yoloコマンドで学習を実行する場合のBashでのスクリプト例を示します。YOLOv8ではモデルのバリエーションとしてYOLOv8nからYOLOv8xまで5種類ラインナップがあり、ここではYOLOv8mを使用していますが、Cleanlabはモデルに依存しない手法なので基本的にどのモデルを選択しても問題ありません。実行する計算機環境やどれぐらいの時間でスコアを算出したいかに応じて選択してください。 for i in {1..5}; do yolo train data=/usr/src/datasets/coco_fold${i}.yaml model=yolov8m.pt done 予測と正解データの生成 各foldで学習したモデルで各foldのvalidationデータに対する推論を実行し、各推論結果を結合することでデータセット全体に対する予測を出力します。生成した予測はCleanlabが入力として期待するフォーマットに変換する必要があります。またvalidationデータに対する正解データの生成も必要です。 YOLOv8を用いて予測と正解データの生成を実行するサンプルスクリプトは以下の通りです。出力される predictions.pkl が予測、 labels.pkl が正解データに対応しています。 import os import pickle import numpy as np from tqdm import tqdm from ultralytics import YOLO from ultralytics.data.dataset import load_dataset_cache_file from ultralytics.engine.results import Results def make_prediction (results: Results, num_classes: int = 80 ) -> np.ndarray: """YOLOv8の推論結果をCleanlabの入力形式に変換""" pred_cls = results.boxes.cls.cpu().numpy() pred_conf = results.boxes.conf.cpu().numpy() pred_boxes = results.boxes.xyxy.cpu().numpy() prediction = [] for target_cls in range (num_classes): target_cls_mask = pred_cls == target_cls if sum (target_cls_mask) == 0 : # 該当クラスの検知結果が無い場合の処理 prediction.append(np.empty(( 0 , 5 ), dtype=np.float32)) else : target_cls_boxes = pred_boxes[target_cls_mask] target_cls_conf = pred_conf[target_cls_mask] target_cls_pred = np.concatenate([target_cls_boxes, target_cls_conf[:, None ]], axis= 1 ) prediction.append(target_cls_pred) return np.array(prediction) def make_label (data: dict ) -> dict : """YOLOv8の正解データをCleanlabの入力形式に変換""" label = { "labels" : data[ "cls" ].flatten().astype( int ), "seg_map" : os.path.basename(data[ "im_file" ])} # 正規化されたcx, cy, w, hの2次元アレイをshapeの値で元の座標に戻して、xyxy座標に変換 bboxes = data[ "bboxes" ] * np.array([data[ "shape" ][ 1 ], data[ "shape" ][ 0 ], data[ "shape" ][ 1 ], data[ "shape" ][ 0 ]]) bboxes[:, 0 ] = bboxes[:, 0 ] - bboxes[:, 2 ] / 2 bboxes[:, 1 ] = bboxes[:, 1 ] - bboxes[:, 3 ] / 2 bboxes[:, 2 ] = bboxes[:, 0 ] + bboxes[:, 2 ] bboxes[:, 3 ] = bboxes[:, 1 ] + bboxes[:, 3 ] label[ "bboxes" ] = bboxes return label def main () -> None : """5-foldのOut-of-fold予測および正解データを生成""" model_paths = [ "/usr/src/ultralytics/runs/detect/train/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train2/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train3/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train4/weights/best.pt" , "/usr/src/ultralytics/runs/detect/train5/weights/best.pt" , ] dataset_paths = [ "/usr/src/datasets/coco_fold1/labels/val.cache" , "/usr/src/datasets/coco_fold2/labels/val.cache" , "/usr/src/datasets/coco_fold3/labels/val.cache" , "/usr/src/datasets/coco_fold4/labels/val.cache" , "/usr/src/datasets/coco_fold5/labels/val.cache" , ] predictions: list [np.ndarray] = [] labels: list [np.ndarray] = [] for model_path, dataset_path in zip (model_paths, dataset_paths): model = YOLO(model_path) cache = load_dataset_cache_file(dataset_path) for data in tqdm(cache[ "labels" ]): results = model(data[ "im_file" ]) predictions.append(make_prediction(results[ 0 ])) labels.append(make_label(data)) # pickleファイルとしてpredictionsとlabelsを保存 with open ( "predictions.pkl" , "wb" ) as f: pickle.dump(predictions, f) with open ( "labels.pkl" , "wb" ) as f: pickle.dump(labels, f) if __name__ == "__main__" : main() Cleanlabによる品質スコア算出 validationデータに対する予測と正解データを比較して画像ごとの品質スコアを算出します。品質スコアは0.0から1.0の範囲の数値で出力され、数値が高いほどアノテーション品質が高いことを表します。先ほどのスクリプトで predictions.pkl と labels.pkl が生成されているはずなので、これをCleanlabの 物体検出向けのAPI に入力します。Cleanlabは pip install Cleanlab 等でインストールできます。 スコアを算出し、スコアの低い画像を確認するサンプルコードは以下の通りです。COCOは80クラスのデータセットですが、説明のしやすさのためpersonクラスのみに絞った場合の例になっています。このコードを Jupyter Notebook 等で実行することで品質スコアの低い画像およびアノテーションを確認することが出来ます。 import pickle from Cleanlab.object_detection.rank import ( get_label_quality_scores, issues_from_scores, ) from Cleanlab.object_detection.summary import visualize IMAGE_PATH = 'all_images' predictions = pickle.load( open ( "predictions.pkl" , "rb" )) labels = pickle.load( open ( "labels.pkl" , "rb" )) # 各画像の品質スコアを計算 scores = get_label_quality_scores(labels, predictions) # 品質スコアが0.5を下回る画像インデックスをスコアの低い順にして取得 issue_idx = issues_from_scores(scores, threshold= 0.5 ) # 一番スコアの低い画像を表示 class_names = { "0" : "person" } issue_to_visualize = issue_idx[ 0 ] # ここの数値を変更することで別の画像を表示可能 label = labels[issue_to_visualize] prediction = predictions[issue_to_visualize] image_path = IMAGE_PATH + label[ 'seg_map' ] visualize(image_path, label=label, prediction=prediction, class_names=class_names, overlay= False ) 私の手元で試したCOCOデータセットにおけるいくつかスコアの低い画像および高い画像の例を載せておきます。図中の左画像の赤枠が正解データ、右画像の青枠がモデルによる予測です。 スコアの低い画像の例 右画像のモデルは左奥にいる人を予測できているにも関わらず、左画像ではアノテーションがされていないことでスコアが低くなっています (スコア: 2.092e-34) この画像は Timothy Krause氏によるMan with umbrella をCleanlabにより可視化したものです。 (C) 2012 Timothy Krause, Man with umbrella, License: http://creativecommons.org/licenses/by/2.0/ 右画像のモデルは4人の人物を正確に予測していますが、左画像では4人を囲うようにアノテーションされているためスコアが低くなっています (スコア: 4.821e-07) ちなみにCOCOでは複数人をまとめてアノテーションする場合にiscrowdという属性をTrueにするというルールがありますが、この画像はiscrowdがFalseになっていたので、そういった観点でもアノテーションに不備があると言えます この画像は Luca Vanzella氏によるDSCN0073b をCleanlabにより可視化したものです。 (C) 2005 Luca Vanzella, DSCN0073b, License: https://creativecommons.org/licenses/by-sa/2.0/ スコアの高い画像の例 正解データと予測がほぼほぼ一致しているのでスコアが高くなっています (スコア: 0.987) この画像は Michael Elleray氏によるSurfin' をCleanlabにより可視化したものです。 (C) 2011 Michael Elleray, Surfin', License: http://creativecommons.org/licenses/by/2.0/ この画像は Wonderlane氏によるMan making a sandwich, Vietnamese / Chinese, International District, Seattle, Washington, USA をCleanlabにより可視化したものです。 (C) 2010 Wonderlane, Man making a sandwich, Vietnamese / Chinese, International District, Seattle, Washington, USA, License: http://creativecommons.org/licenses/by/2.0/ アノテーション修正 スコアの計算が出来たらスコアの低い画像群の確認と修正を実施します。アノテーションツールとしてセーフィーでは FastLabel社 のツールをよく利用しています。その他 CVAT やYOLOv8公式でサポートされている Roboflow 等、様々なツールがありますので利用しやすいものを使用してください。 Cleanlabをフル活用するためのTips 今回Cleanlabを利用する中で気づいた課題やそれに対する対策についていくつか説明します。 同じ正解データや予測を複数回マッチングさせないようにする Cleanlabの現状の実装(2024/05/17時点)ではスコア計算時に同じ正解データや予測が複数回マッチングされる場合があり、スコアが正しく計算できない場合があります。スコアを出力してみて複数回マッチングの影響で意図しないスコアが出ているようであれば、スコア計算処理のマッチング部分を修正する必要があります。 閾値をモデルやデータセットに応じて適切に設定する Cleanlabでは信頼度の低い予測をスコア計算に使用しないように2つの閾値を設けています。この閾値の設定によって出力されるスコアの傾向が大きく変わるので、自分が学習したモデルやデータセットの特性に応じて調整することで精度の高いスコア計算が可能になります。 スコアが1.0になっている画像でも一通り確認する Cleanlabのスコアは0.0から1.0までの数値で表されるため、定義としては1.0は最もスコアが高い(アノテーション品質が高い)ことになります。しかし正解データと予測でマッチングするのものが無かった場合等にCleanlabはスコアのデフォルト値である1.0を出力するため、1.0というスコアは必ずしもアノテーションに誤りが無いことを表してはいません。実際上正解データと予測がぴったり合う(スコアが1.0になる)ことはほとんどあり得ないので、1.0のスコアが出力されたということは何らかの問題がある可能性があります。 以下の画像は一例ですが、スコアが1.0にも関わらず正解データではアノテーションが1つ漏れていることが分かります。これは予測のbboxの信頼度が低く、正解データとのマッチングプロセスがスキップされるために発生していると考えられます。 この画像は Brad Greenlee氏によるDonut Tower II をCleanlabにより可視化したものです。 (C) 2007 Brad Greenlee, Donut Tower II, License: http://creativecommons.org/licenses/by/2.0/ まとめ 今回はCleanlabを用いたData-centric AIによる物体検出データセットのクレンジング方法について紹介しました。Cleanlabを利用することで効率的にデータセットを改善することが出来、モデルを変えなくてもモデルの精度改善が可能になります。実際の改善では今回紹介したData-centricおよびModel-centricの両方の観点で取り組むことが重要ではありますが、Model-centricアプローチによる精度改善に手詰まりを感じられている方がいれば試してみるのも良いと思います。 最後になりますが、セーフィーではエンジニアを積極的に募集しています。気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 最後までお読みいただき、ありがとうございました。
アバター
こんにちは!23新卒のフロントエンドエンジニアの一氏です。 新卒エンジニアの研修の一環として、社内課題の解決をテーマに企画から実装まで行う自由度の高いチーム開発研修を実施しました。 チーム開発研修を行う過程で私たち5人の新卒エンジニアはどんなことを考えていたか、何を学んだかをまとめました。 研修概要 目的 課題 チーム開発研修 研修の流れ アイディア出し 開発初期にしたこと 開発 成果物詳細 こだわりポイント 学びが深かったこと どの機能をどこまで作り込むか フロントエンド開発 アジャイル開発 もっとこうすればよかったこと 報連相の重要性 APIの仕様のすり合わせ 最後に 研修概要 目的 今回のチーム開発研修を行うにあたって、初めに以下の3つの目的が共有されました。 セーフィーで取り扱っているカメラの内部構造についてエンジニアとして理解しておく システム開発で補助をもらいながら自走可能な状態になる 業務(チーム開発)で必要なスキルを磨く(工数・チケット管理等) 特にカメラの内部構造の理解はハードウェアを扱っている弊社ならではの目的であり、面白いポイントだと思います。 課題 提示された課題は「社内課題の解決 *1 」でした。 具体的な社内課題のテーマや開発手法、開発環境などは新卒が自由に決めることができました。 制約として、セキュリティ担保のためのインフラチームによる設計レビューやRaspberry Pi(カメラ)の利用などは課せられましたが、それ以外は自由です。 目的・課題が与えられた当初はこんなに自由度高く企画から開発まで研修で行うのかという驚きが半分、新卒が企画から開発まで全てを通して実施することができるのかという不安が半分でした。 また研修で企画から開発まで一通りの流れを経験することは、配属後の業務に役に立つだろうという期待もありました。 チーム開発研修 研修の流れ 期間は2023年6月12日〜7月31日までの1ヶ月半ほどで、以下の流れで行いました。 アイディア出し 初めに何の社内課題をどのように解決するかアイディア出しを行いました。 どんな課題があるのか調査し、マインドマップを作成し、アイディアを煮詰めていきました。 アイディア出しで難しかった点は3点あります。 1点目は、フロント・サーバー・デバイスの3つの領域をカバーすることで、新卒エンジニアメンバーの配属先と同じ領域をカバーしたいと考えたためです。 2点目は、目的の1つであるカメラの内部構造の理解のためにデバイス(Raspberry Pi)の利用が必要だったことです。 3点目は、実際に私たち自身が使いたくなるようなアイディアであることで、社内課題の解決のために開発するならこれから先本当に社内で使ってくれるものを開発した方がやる気が出ると考えたためです。 上記の難しかった点と社内課題の解決というある意味選択肢が無限にある課題を満たすため様々なアイディアが出てきました。 最終的にチーム開発のテーマは弊社の福利厚生としてある社内図書を、映像から顔認証と書籍のバーコード読み取りを通して貸し借りを行う、書籍管理システムの作成になりました。 開発初期にしたこと アイディアが決定した後はプロジェクトとして進めていくのに必要なことを実施・決定していきました。具体的には以下の内容です。 プロジェクト計画書作成 要件定義書など各種仕様書作成 顔認証などの技術検証 環境構築 開発手法決定 各種仕様書の作成は新卒エンジニア全員がほぼ経験なし or 未経験で、どのように作成すれば良いのか手探りで進めていきました。 チーム開発研修の前にUdemyでプロジェクト管理の講座でどのような仕様書が必要か学んでいましたが、講座を見るのと実際に作成するのとでは勝手が大きく違いました。 特に1ヶ月半という短い期間でどの仕様書をどこまで作り込むべきか悩みながら作成しました。 仕様書を作成した後で、この部分は今回の研修では考えなくてよかったということや、初めから完璧な仕様書を作ろうとして想定以上に時間がかかってしまったなどの反省点がありました。 途中からは不完全でもある程度仕様書を作成した時点で上司にレビューしていただき、方向性が間違っていないか確認するようにしました。 早めにレビューしていただくことで、余計な手戻りを少なくしつつ仕様書作成が進められることを学びました。 今回作成するプロダクトは研修の制約としてRaspberry Piの使用が必須であるため、フロント・サーバー・デバイスの3つの環境構築が必要でした。 フロントは今回はフレームワークに頼らずできるだけ地力をつけたいという思いからフレームワークなしのTypeScriptとWeb Componentsで実装しました。(ご想像の通り茨の道でした。) サーバーは弊社で採用されることが多いFastAPIとしました。また、書籍の貸出履歴の閲覧機能などの開発工数を抑えられるようにNotion APIを利用しました。 Notion APIを採用することでNotionのデータベースをAPI経由で操作できるようになります。 デバイスは初めC++で環境構築していたのですが、購入したRaspberry Pi カメラモジュール V3がC++で動かず… Pythonでは動作したため、デバイスで使用する言語をPythonに変更しました。 Pythonに変更する事に合わせて、サーバー自体もFastAPIを使用したローカルAPIサーバーに変更することにしました。 開発 開発はアジャイル開発で進めました。今回は1スプリントを2週間として、2スプリント回すことになりました。 具体的に学んだことは後述しますが、困ったことを気軽に相談できる環境や1スプリントで何をするのかを明確にすることなどを学べたことは、今後様々な開発を行う中で役に立つと思います。 開発中に難しい箇所があった際は、フロント・サーバー・デバイスの各領域のエンジニアに相談しながら進めることができました。 実装で困ったことがあった際は基本的にSlackで随時相談しつつ、時には実装のレビューをしていただきながら進めました。各領域のエンジニアに相談できる環境は非常に心強かったです。 最後の1週間はリリースの準備として、デプロイ環境の整備や利用者向けのドキュメント作成、発表資料作成、バグ修正を行いました。リリースに向けて行うことも多く、ギリギリまでドタバタしながら作業していました。もちろん実際のサービスのリリース作業と比較すると簡単なリリース作業とは思いますが、リリースのためにする作業を一通り経験できたのはよかったです。 成果物詳細 最終的な成果物である書籍管理システム librarian は主に以下の機能を提供するサービスです。 顔認証および書籍のバーコードスキャンを用いた書籍の貸出・返却 書籍一覧の閲覧はNotionで可能 初めにユーザーは貸出か返却かを選択します。 開始画面 顔認証は事前のユーザー登録時に顔画像を登録し、登録した顔画像とスキャンした顔画像の特徴が同じか FaceNet で識別しています。 顔画像はAWSのS3上に特徴量として保存しています。 当初はRaspberry Pi上で顔認証処理を行っていましたが、処理に5秒ほどかかっていました。 サーバー上で顔認証処理を行うように変更した結果、顔認証の処理時間は1.5秒ほどになりました。(今回は社内でのみ使うシステムだったためサーバー上で顔認証処理を行うという力技で解決しました。) 顔認証成功画面 書籍のバーコードスキャンは pyzbar を用いて映像からバーコードのISBN番号を取得しています。取得したISBN番号からNotionの書籍一覧データベースの書籍情報を取得して表示します。 バーコードスキャン画面 顔認証と書籍のバーコードスキャンをした後は貸出・返却ボタンをクリックして貸出・返却処理を行います。貸出・返却処理が完了するとNotionのデータベースが更新されます。 確認画面 貸出・返却処理完了画面 なお今回は時間の問題からユーザー登録・削除はAPIのみ作成しSwagger UIから直接APIを叩き、書籍登録・削除はNotionデータベースに直接入力としました。 こだわりポイント 作成するにあたって特にこだわって作成した点は以下の2点です。 顔認証・バーコードスキャン機能による直感的なUI/UXの提供 Notionデータベースによる書籍一覧と貸出履歴の閲覧 カメラで撮影した映像から顔認証とバーコードスキャンを行うことで、貸出・返却の際に利用者カードのような持ち物は必要なく、ログインや書籍の検索などの面倒な操作なしで利用できます。 開発する際は実装前に顔認証やバーコードスキャンをした時の動作イメージを考えるため、事前にFigmaでプロトタイプを作成し、UI/UXはどうかトレーナーにも確認していただいてから実装に入りました。結果としてほぼイメージ通りの動作を実現することができました。 また、Notionデータベースを利用したことは開発工数の削減に大きく貢献しました。 Notionのデータベース機能を利用することで、データベースに関する開発と書籍一覧等の表示に関する開発の工数を削減することができました。Notionのデータベースはフィルターや並べ替え、検索機能もついており、これらの機能も含めて実装する場合は研修期間内に開発を完了させることはできなかったと思います。 作成した書籍管理システム librarian は今後ユーザー登録や書籍登録、社内への周知などを行ない、現在は実際に社内で運用されております。 学びが深かったこと どの機能をどこまで作り込むか どの機能をどこまで作り込むかに関しては1ヶ月半という開発期間ではごく一部の機能しか作り込むことはできませんでした。 開発当初は追加機能についても多くのアイディアが出ていたのですが、実際にはほぼ必須の機能しか実装することはできませんでした。 一般的には開発の難易度や開発期間、メンバーの習熟度などをふまえて、どのくらいの機能を作ることができるかを決められると思います。 今回の開発ではこれらをふまえてどの機能まで作るか目安を考える力が不足しており、1ヶ月半の期間で開発できる機能を超えて開発計画を立ててしまいました。 開発計画が予定通り進行していないことは1スプリントでどのような成果物ができたかを確認するスプリントレビューで気づきました。今回は研修期間を伸ばすことはできなかったためMVPを作成することを最優先として、実装しなくても良い機能はどれか改めて議論することになりました。 最終的にユーザー登録・削除機能はAPIのみ開発してフロントエンドの開発をしないことにして、なんとかMVPを作成することができました。 実際の開発ではQCDの観点もふまえて開発する機能を決定していくと思います。今回の研修で改めてプロジェクト計画を予定通りに進行させる難しさを学びました。 フロントエンド開発 今回フロントエンドはフレームワークなしのTypeScriptとWeb Componentsで実装することにしました。 弊社ではサービスによってAngular, Vue, Reactのフレームワークを使い分けており、フロントエンド開発の地力の部分を学びたいという思いから、フレームワークなしという選択をしました。 しかしフレームワークなしでSPAを実装したことがあるメンバーはおらず、どのように実装すれば良いのか初めは検討もつけることができませんでした。 そんな中メンバーの一人がフレームワークなしで開発する方法を調べてベースとなる開発環境を構築してくれたのですが、この開発環境のおかげでスムーズに開発に入ることができました。 作成した開発環境には事前にTSとテンプレートの組み合わせ方、ライフサイクルメソッドの使い方、コンポーネント化の方法の例が書かれていました。この例やその他のWeb Componentsでの実装方法を参考にしつつ実装を進めたことで、フロントエンドの開発のよりコアな部分の知識を身につけることができました。 アジャイル開発 今回メンバー全員が初めてのアジャイル開発に取り組みました。 弊社ではアジャイル開発を採用しているチームが多いことや内定者インターンの際はウォーターフォール開発を採用したことからアジャイル開発を採用しました。 1スプリントを2週間として、デイリースタンドアップ、スプリントプランニング、スプリントレビュー、レトロスペクティブといったスクラムイベントを実施しました。 アジャイル開発は初めてということもあり、1スプリントでどのくらいのポイントを消化できるのか、開発の方法をどのように改善していくのか手探りで進めていきました。 みんながアジャイル初心者だったことでお互いに進め方でわからない部分を気軽に相談することもできました。 今回はスプリントを2周しか回すことができなかったのですが、1周目のスプリントに比べて2周目のスプリントはベロシティを1.2倍に増やすことができました。 ベロシティが増えた要因として、だんだんと開発に慣れてタスクの抜け漏れが減ったこと、わからない部分はお互いにすぐに相談することで素早く問題解決できるようになったことが挙げられると思います。 もっとこうすればよかったこと 報連相の重要性 開発を進める中で報連相の重要性を実感しました。 例えばプロジェクト計画書の作成で上司にレビューをしてもらう必要があったのですが、初めからある程度完成したものを提出しようとしていました。 しかし、レビューをしていただくと今回のチーム開発では必要な項目が足りなかったり不必要な項目があったりしました。 どの項目が必要かを事前に相談してから作成すれば大きな修正は少なくなったと思います。 また、開発中に実装方法で悩んだ際に一人で悩み続けてしまうことがありましたが、結局他のメンバーやトレーナーに相談するとすぐに解決するということが何回かありました。 これらの経験から自分だけで悩み続けるのではなく、早めに他のメンバーやトレーナー、上司と方向性の確認や相談をすることが大切なことを再認識しました。 今回のチーム開発研修では特に研修の初期はなかなか相談することができませんでしたが、開発が進むにつれてなんでも相談できるようになってきました。 しかし、どのくらい悩んだら相談するかのラインを引くのは個人的に苦手意識があります。 なんでも相談しすぎると周りの時間を奪うことになり、相談しなさすぎるといつまでも同じ箇所で悩んで自分の時間を浪費することになります。 15分悩んだら相談するというようなルールをチームで決めることが開発を行う中で重要と感じました。 APIの仕様のすり合わせ 今回の開発の1番の反省点としてAPIの仕様のすり合わせに時間がかかったことが挙げられます。 APIの仕様でメンバー間で齟齬があったことや考慮漏れがあったことで後でAPI仕様書を修正することがありました。原因として以下の点が考えられます。 API仕様書の作成とAPIの実装を同時に行った API仕様書のレビュー方法が曖昧だった 今回APIはFastAPIで作成しましたが、FastAPIはSwagger UIを用いたAPIドキュメントを作成することができます。 今回はSwagger UIをそのままAPI仕様書とすることにしたのですが、どこまで実装してからAPI仕様書としてレビューするのかを明確にしないまま開発を進めてしまいました。 その結果、API仕様書のレビューをせずにいきなり実装まで行い、後からAPIの修正が必要な場面が何度かありました。API仕様書としてレビューできる最低限の部分だけ実装してAPI仕様書のレビューを行うというルールを作ることで防げたと思います。 また、API仕様書はトレーナーからのレビューが必須だったのですが、メンバー間でのレビューを必須としていませんでした。 トレーナーからのレビューは主にAPI仕様書の形式が問題ないかをレビューしており、APIの細かいパラメータやURIが実装をする上で問題ないかのレビューを行なっていなかったため、後からパラメータの不備などに気づくことがありました。 パラメータやURIについてはトレーナーなだけでなく、実装を行うメンバー間でレビューを行うなど、誰がどのような観点でAPI仕様書のレビューを行うのかを明確にすることが必要だったと感じました。 最後に 今回のチーム開発研修は1ヶ月半という短い期間で企画からリリースまで開発の流れを一通り行うことができる貴重な経験となりました。今後は研修で学んだことを活かして、映像から未来をつくるシステム開発に取り組んでいきたいと思います。 *1 : 本研修における社内課題の解決とは業務を支援するツールの作成ではなく、社内の環境を快適にするための何かを作成することです。
アバター
はじめまして! エンジニアリングオフィスの井上です。 本記事では、開発本部(エンジニア向け)の社内アイデアソンの企画運営に携わりましたので取り組みの様子や内容についてお伝えさせていただこうと思います! 突然ですが「どこでもドア」って売れると思いますか? そもそも「アイデアソン」ってなに 開発本部でアイデアソンをする目的 アイデアソンのテーマ アイデアソンの日程 Day1で何をしたか 使用したビジネスフレームワーク Day1終了後からDay2までの一週間 Day2で何をしたか Day2結果発表 アイデアソンを実施した感想 参加者からの感想 社内アイデアソンに参加した感想を教えてください 最後に 突然ですが「どこでもドア」って売れると思いますか? いきなりですが某猫型ロボットでおなじみの「どこでもドア」、もし製品化したら売れると思いますか?みんな欲しいから売れるに決まってるよね、と自分は考えていました。 では、「どこでもドア」の価値はどのようなものでしょうか? 下準備無しで任意の場所へ瞬時に往来できる、というのが「どこでもドア」の価値ですよね。この価値に釣り合う価格は、一体どうやって決めるのでしょうか?難しいですよね。 価格設定やルールを作るためには、主要なターゲットを明確に定義する必要があります。 仮に「どこでもドアが」販売されたとしたとしたら国際問題へと発展する可能性があります。価値の項でも記載しましたが、任意の場所へ瞬時に往来できる道具があったら使い方次第で国を跨ぐ大犯罪をリスク無しで実行できてしまいます。 上記を踏まえて考えてみると、使用に際して法整備も必要になるので顧客は一般の方や法人かもしれませんが、ステークホルダーとして政府や各国との調整が必要です。 また、利用規約に違反した人を取り締まる仕組みや機関も必要でしょう。 全世界で利用された場合のメンテナンスやケアはどうなるでしょうか。 etcetc….. 軽く考えただけでも課題が山積みです。 どこでもドアのように、凄まじく便利な物があったとしても実際に「売る」となると簡単ではない、売るのは難しいな・・・と自分は思いました。 日頃のエンジニア業務だとアイデアを出した後に深堀し続けて、どう売るか?顧客は?販売チャネルは?という事を考える機会はあまりありません。 そのための機会として、開発本部のエンジニアを対象にアイデアソンを開催しました。 そもそも「アイデアソン」ってなに アイデアソンは「アイデア(idea)」と「マラソン(marathon)」をかけ合わせた造語で、みんなで集まり議論を行いながら様々なビジネスアイデアを出し合う、という取り組みです。 似たような言葉に「ハッカソン(hack + marathon)」があります。 ハッカソンは「開発」に重きを置くのに対して、アイデアソンは「アイデア」に重きを置いています。 そのため、ハッカソンに比べて専門知識が必要なく、誰でも参加しやすいというのがアイデアソンの大きな特徴になります。 開発本部でアイデアソンをする目的 セーフィーの開発本部は2024年4月現在、以下のように職能にて組織が分かれています。 ※セーフィーの組織についてより詳しく知りたい方は、是非引用元のVPoEの仕事について - Safie Engineers' Blog! をご参照ください! 一般的には、組織というのは職能別に分かれているため、職能外の技能・知見は吸収し難いという課題があります。その課題を解決するためにも、一緒にプロダクトの将来を考えるアイデアソンをチーム横断で実施することにより、より人となりを知ったり、社内ネットワーキングの向上を一つの目的としました。 なので、参加者は応募形式とし、できるだけ同じ部署にならないようばらけたチーム構成にしています。 また、アイデアがあってもそれをサービス化するのは難しいため、アイデアをサービス化するためにファクトとして何が必要か、という事を体系立てて学ぶ事ができる機会を提供することを目指しました。 アイデアソンのテーマ Safieの映像プラットフォームを活かし、新しいビジネスアイディアを創出しよう! というものです。 参加者の関心がある課題の解決のために、セーフィー株式会社がどのようにアプローチできるか、という事を主軸に置いて、今回のテーマを設定しました。 また、セーフィーが展開している「映像プラットフォーム」を利用することで、何が出来るのか・実現できるのかということをアイデアソンを通して再発見してもらう事も本テーマを設定した理由の1つです。 アイデアソンの日程 今回の社内アイデアソンは一週間の期間で開催しました。 初日(Day1)はキックオフ、最終日(Day2)に発表日を設定し、それ以外の期間中はチームごとに集まって相談・ブレストして資料作成を進める方針にしました。 Day1で何をしたか まず、Day1にて各チームでアイデア出しをしてもらい、これぞ!というアイデアをチームの発表内容に選定してもらいます。 じゃあ各々始めてください!といって放り出す事はせず、アイデアを出す際の考え方や、アイデアをブラッシュアップするための手法・ツールについての座学を準備しました。 本アイデアソンでは、LeanCanvasというビジネスフレームワークも用いてアイデアの深堀や構造化を行いました。 各チームでアイデアの相談をしている風景 使用したビジネスフレームワーク Lean Canvasとは、新しいビジネスモデルの開発を生産効率性の向上と問題の顕在化によるムダの徹底的排除というアプローチで目指すマネジメント論を指します。 LeanCanvasツール このツール上の各項目を埋めていく事でアイデアを短時間でシンプルに可視化・検証できるため、効率的なブラッシュアップが可能で認識共有が容易といった利点があります。 上記のフレームワークを利用しつつ、実際にチーム内で現実問題として困っていること、改善されたら便利になることを挙げてもらい、セーフィー株式会社の得意分野である「映像」を用いて解決に持っていくことを模索しました。 実際にアイデアを出し合い、各チームで相談している際の風景 各チームメンバーから秀逸なアイデアが生まれ、選定に苦労していました Day1終了後からDay2までの一週間 Day1で各チームのアイデアが確定したので、Day2までの期間でプレゼン資料の作成に取り組みました。 この期間中Day1で実施した講義内容も踏まえ、各チームメンバーが役割分担を行い、競合調査・市場調査・実現性を高めるための課題の洗い出し等を実施しました。 Day2で何をしたか Day2では、各チームで作り上げたアイデアの発表を行いました。 発表の場では、実際にセーフィーの役員・部長陣がビジネスからの観点やプロダクトとしての実現性等を加味して審査頂きました。 部室長の前で練りに練ったアイデアを発表している様子です Day2結果発表 栄えある第一回社内アイデアソン優勝チームは、チームAでした! アイデアの内容については細かな内容を公表することはできませんが、優勝の決め手として、「未来の意思決定に大きく作用することができる」という所に審査員が未来と発展性を感じたからでした。 アイデアソン終了後の集合写真。無事成功して安堵しました アイデアソンを実施した感想 今回の社内アイデアソンを振り返ってみると、開発本部でアイデアソンを開催する目的である アイデアをサービス化するために体系立てた学びの機会を提供する チーム横断的な社内ネットワーキングの向上 この2つについて、間違いなく達成できたと感じています。 ・アイデアをサービス化するために体系立てた学びの機会を提供する アイデアソンを行うのは初めてだったので、業務時間内でアイデアやアウトプットができるか、といった不安がありました。 実施した結果、参加者全員が楽しみながらアイデア出しや資料作成をされており、全体を通して良いアイデアソンになったと思います。 チームで選定したアイデアは勿論、個々人のアイデアについても、これがあったら世の中もっと便利に・安全になる!実現したい!という熱量が感じられる物でした。 個々人の中で貯めこんでいたアイデア・思い・仕組みを具現化できるのはアイデアソンならではだと思います。 運営側ではありますが、自分も「こういう考え方があるのか」「人の目を釘付けにするプレゼン資料はこう作るのか」といった学びを得る事ができました。 ・ チーム横断的な社内ネットワーキングの向上 他部署の方々と気軽に相談・会話ができる方が増え、社内交流に寄与できたと実感できました。 既に関係値があるメンバーの新しい一面を見ることができるので、社内イベントとして実施するのはかなり有効だと感じました。 懇親会では「あのアイデア良かったね!」という言葉もあれば、「あのアイデアに既存のプロダクトと合体させたらもっと魅力的になるんじゃないか」といったフィードバックもあり、盛り上がりました。 エンジニア同士が交流を深め切磋琢磨できる環境を提供でき、実りのある経験を得られたと執筆しながら感じています。 参加者からの感想 参加者の皆さんに記入いただいたアンケートの感想を公開します。 社内アイデアソンに参加した感想を教えてください アイデア出しから、ターゲット選定、実現性判断、プレゼン資料作成まで一連の流れを経験できて学びになった。 他のグループの発表を聞いて、この部分もう少し深堀できたななど、振り返れるところが大きかった。 普段とは違った頭の使い方ができて楽しかったです。マネーの虎みたいでした。 自分がこういう製品が欲しいなと思ったものを他の人の意見をもらいながら、あーでもないこーでもないとブラッシュアップして、最終的にプレゼン出来るところまで持っていけたのがとても良かった。 普段使わない頭の使い方をすることができた。リーンキャンパスなどのフレームワークは実際に使ってみないと理解できないので良いきっかけだったと思う。 全体的に面白かったです。参加した人は、「こういうサービス作りたい、必要だ!」と本気で思って参加されているように思ったので、出た案が企画に回されて、具体的なサービスとして実現される可能性がもっと感じられれば良かったと思います。 アイデアソンを通して、他の人のアイデア・資料の作り方の考え方を実際に見て体感できたのはプラスだった。 普段関係値が無い人と一緒に作業を行う事で、コミュニケーションを取れる機会になったのがよかった。 最後に 本記事では、社内アイデアソンについてご紹介させていただきました。 セーフィーではエンジニアを積極的に募集しています。どのような職種があるのか気になる方はこちらをご覧ください! https://safie.co.jp/teams/engineering/ カジュアル面談から受け付けておりますので、気軽に応募いただければと思います! 皆様のご応募、心よりお待ちしております! 最後までお読みいただき、ありがとうございました。
アバター