はじめに
はじめまして!2023年3月に ABEMA で1ヶ月間「CA Tech JOB」というインターンシップでバックエンドエンジニアとして働かせていただきました、山田航生 (@koki-algebra)と申します。本ブログ執筆時は北海道大学情報科学院の1年生で、大学院入学前のインターンシップでした。
大学では連合学習 (Federated Learning) という分散環境における機械学習に関する研究を行っています。また、私は学業の傍ら同大学発スタートアップ企業でバックエンドエンジニアもしています。そのため、パブリッククラウドによるインフラの構築や API サーバの設計および実装の経験はある程度ある状態での応募となりました。
今回のインターンシップは「Hayabusa」という CyberAgent が内製している画像処理の SaaS の廃止に伴い、ABEMA で独自に開発することなった画像処理基盤の実装の一部を担当させていただきました。
本ブログでは実際に行った業務内容の解説と、インターンシップ中にたくさんの方とお話をして感じたことを書いてみたいと思います。
業務内容の紹介
背景
Hayabusa は CDN の機能などを利用してフォーマット別の画像を生成や画像のリサイズなどを行う社内向けの SaaS です。その Hayabusa が廃止になることが決まったため、Hayabusa に依存しているさまざまなサービスは対応に追われている状況です。ABEMA では Go と CDN を用いて画像処理基盤を自作し、Hayabusa を置き換えることになりました。ABEMA ではこれを「Hayabusa 移行 (Hayabusa Migration)」と呼んでいます。
今回のインターンシップではこの Hayabusa 移行作業のうち、複数枚の画像を重ね合わせて合成する「オーバーレイ」という機能の開発を担当することとなりました。
ライブラリの選定
さて、Go で画像合成を行うにはどんなライブラリを用いるのが良いでしょうか。今回の要件を実現するのに最適なライブラリを選定することから始めました。
現行の Hayabusa は裏で Cloudinary を使って画像合成を実現しています。ライブラリの選定の基準としてはまずこの Cloudinary の画像合成を再現できることが条件になります。
また、パフォーマンスやメンテナンスの頻度も重要です。特にパフォーマンスに関しては数百メガバイトといったサイズの大きな画像も高速で処理することが求められます。
有名な画像処理ツールに OpenCV があります。OpenCV は Intel が開発している OSS であり、基本的な画像処理はもちろん、顔認識といった画像認識の機能も有しています。そんな多機能の OpenCV ですが、今回の要件には少しオーバースペックであり、Go で扱うには OpenCV 本体のインストールが必要で、環境構築が面倒な印象があったので今回は OpenCV は採用とはなりませんでした。
それでは、Goには他にどのようなライブラリがあるのでしょうか。Awesome Go などのサイトを参考に探してみましたが、メンテナンスが頻繁にされているサードパーティのライブラリは見つかりませんでした。
実は Go には image という基本的な画像処理ができるコンパクトな標準ライブラリがあります。 image パッケージには画像の表現や操作を行うためのインターフェースが定義されています。たとえば、Image インターフェースは、画像の幅と高さを取得するBoundsメソッドと、指定した座標の色を取得するAtメソッドを提供します。このインターフェースを実装することで、カスタムの画像形式や画像変換を実現することができます。
また、image パッケージには、画像の変換を行うための関数や構造体が定義されています。たとえば、Image インターフェースを実装した画像を、異なる色空間に変換する color パッケージと連携するための RGBA 型や、画像を拡大縮小する Resize 関数があります。
image/draw は画像の描画や合成を行うための機能を提供するパッケージです。このパッケージを使用することで、線や図形、テキストなどを描画したり、複数の画像を合成したりすることが可能となります。
また、Go の標準ライブラリであるため、インストールが不要でメンテナンスの保証があります。さらに、公式ドキュメントも豊富であり、問題が発生した場合には解決策を見つけやすいという利点もあります。
以下は image パッケージを使った画像合成のサンプルコードになります。
``` package main import ( "image" "image/draw" "image/png" "os" ) func main() { // 画像読み込み baseImg, err := loadImage("img/gopher.png") if err != nil { panic(err) } blendImg, err := loadImage("img/beard.png") if err != nil { panic(err) } // 画像合成 & 保存 resultImg := Overlay(baseImg, []image.Image{blendImg}) file, err := os.Create("img/result.png") if err != nil { panic(err) } if err := png.Encode(file, resultImg); err != nil { panic(err) } } func Overlay(baseImage image.Image, blendImages []image.Image) image.Image { rect := baseImage.Bounds() canvas := image.NewRGBA(rect) draw.Draw(canvas, rect, baseImage, image.Point{}, draw.Src) for i := range blendImages { draw.Draw(canvas, rect, blendImages[i], image.Point{}, draw.Over) } return canvas } func loadImage(filename string) (image.Image, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() img, _, err := image.Decode(file) if err != nil { return nil, err } return img, nil } ```
The Go gopher was designed by Renée French.
以上をまとめると、次のような理由から、image パッケージはGoを使った画像合成に最適な選択肢であると判断しました。
- Go製のサードパーティライブラリのメンテナンス頻度が低いこと。
- 今回の要件に必要十分な機能が備わっていること。
- 公式が開発・メンテナンスしていること。
スクリプトによる検証
ライブラリが決まったら次は、簡単なスクリプトで既存の Hayabusa と遜色ない画像合成が可能かどうか検証を行いました。
具体的にはローカルの JSON ファイルを入力として、GCP の Go SDK を用いて Google Cloud Storage (GCS) に保存されている画像をダウンロードし、image パッケージを用いて画像合成を行ってローカル保存するというスクリプトを書きました。
自作した合成画像のテスト方法ですが、2枚の画像の差分を画像で出力するスクリプトで評価を行います。テスト用スクリプトは社員の方が過去に Python で実装したものです。テストコードの出力を見る限り合成結果は問題なさそうでした。
さて、この合成処理を API サーバーに実装する際にはパフォーマンスも重要です。実行時間やメモリの使用量は十分に小さいか、数百Mほどのサイズの大きい画像でも問題なく動作するかを検証する必要があります。具体的には pprof を用いて検証しました。
検証の結果 image パッケージはメモリ効率が十分に良く処理も早かったため、画像合成処理はこれで問題ないということがわかりました。
既存のエンドポイントに実装
今回はクライアント側の実装を変更することが難しかったため、既存の画像アップロードのエンドポイントに画像合成機能を実装することになりました。
私は普段スタートアップの開発も1人で行っているため、他人が書いたコードを読む経験があまりありませんでした。また、今回変更を加えるべきレポジトリが歴史的な背景により負債気味になっており、コードを読むのが大変でした。
幸い、読むべき範囲が狭かったのと、トレーナーの方が手厚くサポートしてくれたため、実装は比較的スムーズに進みました。開発環境でのテストも一発で成功しました。
歴史的な背景により、統合テストを書くのが困難だったため、時間的制約もあり、テストコードを書ききれなかったのは残念です。しかし、チーム開発ではプロダクトの品質を担保するために可読性とテスタビリティを意識することが重要だということを認識しました。
それでも、本番環境にリリースすることができたので、まずまずの成果を残せたと思います。
社員の方とお話ししてためになったこと
ここまでインターンシップで実際に行った業務について書いてきましたが、技術的なこと以外でもかなり勉強になりました。
今回のインターンシップでは、ランチや面談などを通してたくさんの社員の方とお話しする機会を設けていただき、特に自身のキャリアについてはかなりの方に相談に乗っていただいたので、そのことについて紹介したいと思います。
キャリアの悩み
一方、最初に申し上げたように私の大学の専門は機械学習であり、卒業研究ではかなりの数の論文を読んで指導教員と相談しつつ自分でテーマを探すなど、自分なりに本気で取り組みました。機械学習には非常に興味があり、理論的な部分まで深く勉強したいと考えています。
元々私は機械学習の研究開発職につきたいと考えて大学院の進学を考えていたのですが、スタートアップで働くうちにバックエンドエンジニアになりたい気持ちが強まりました。今でも研究しているより開発している方が楽しいと感じます。
実際にバックエンドエンジニアのインターンシップに来てみると、エンジニアが必ずしも大学の専門が情報科学とは限らず、文系出身でも活躍されている方がたくさんいました。そこで私は、バックエンドエンジニアとして就職するならば大学院に行く必要がないのではないかと考えるようになりました。
しかし、純粋にバックエンドエンジニアとして就職してしまうと、今まで頑張ってきた数学や機械学習の知識が無駄になってしまう気がしてしまいます。
いろんな方に相談
以上の悩みを多くの方に聞いていただき、次のような意見をいただきました。
まず、修士の2年間を研究に費やしたとしても、学部卒業生とはバックグラウンドが異なることになるため、それは別の価値になると言ってくれました。
また、技術力に差が出るのは当然ですが、2年間の差はすぐに追いつけるということでした。社会人になると勉強する時間がなかなかとれないので、修士で腰を据えて勉強できるいい機会だということも言ってくれました。
さらに、機械学習とバックエンドを両方できる人は貴重だと言われました。バックエンドエンジニアから機械学習エンジニアに移行するのは難しいかもしれませんが、逆は可能なので、研究を頑張ることでキャリアの幅を広げることができるということも教えていただきました。
私自身の考えとしては、修士課程を終えたあとにはバックエンドをメインに、機械学習システムの知識も持ったエンジニアになりたいと考えています。両方を高いレベルで理解するのは難しいことですが、もしできれば非常に高い価値があると感じました。
以上の意見をいただき、私は修士課程に進んで機械学習の研究を頑張ることに決めました。バックエンドと機械学習の知識を統合して高度なシステムを作るために学び続けたいと思います。
おわりに
今まで割と狭いコミュニティにいたので、外に出て優秀な方とたくさんお話できたのは非常にいい機会でした。Web 系は向上心が高く、技術力も高い人が多いとは聞いていましたが、正直予想以上でした。社内の雰囲気も自分に非常に合っていると感じました。
このような貴重な機会を提供してくださった ABEMA のみなさんには感謝の気持ちでいっぱいです。短い間でしたがお世話になりました!