LINEヤフー Advent Calendar 2023の15日目の記事です。
こんにちは。Yahoo!フリマでiOS開発担当をしている村尾(@ymurao2)です。
Yahoo!フリマはiOS17の新機能を用いて、出品画像の背景切り抜き機能をリリースしました。(お知らせ:[iOS版アプリ]iOS 17新機能(背景切り抜き機能など)の追加について)この記事では、「Vision フレームワーク」を活用して出品画像の背景切り抜き機能を実装した事例を紹介します。
背景切り抜き機能とは
Yahoo!フリマは、誰でも気軽に、安心して個人間取引ができるフリマアプリです。Yahoo!フリマでは、ユーザーが商品を出品するとき、商品の画像を撮影します。しかし、その際に余計な背景が写り込んでしまうことから、ユーザーはその余分な背景を削除したいというニーズがありました。
それを解決する手段として、今回iOS17で発表されたLift subjects from images in your appを活用して実装しました。
完成した機能がこちらです。
要件整理
背景切り抜き機能の要件は以下の通りです。
- iOS17以上で利用できる
- 画像編集画面で「背景を削除」ボタンをタップすると、背景切り抜き処理を実行する
- 背景を削除し、新たに「白背景」を適用する
- 白背景が適応された切り抜き画像を使用するか否かのポップアップ画面に遷移する
- 使用する押下後、画像編集画面に戻り、元の画像を背景切り抜きした画像に差し替える
本来は、背景を削除ボタンを押下したとき、画面をブロッキングしたり、切り抜きに失敗したときにエラーハンドリングを行ったりしていますが、本筋とは逸れるため割愛します。
Vision フレームワークについて
The Vision framework performs face and face landmark detection, text detection, barcode recognition, image registration, and general feature tracking. Vision フレームワークは、顔と顔のランドマーク検出、テキスト検出、バーコード認識、画像登録、および一般的な特徴追跡を実行します。
iOS 17の新機能として、VNGenerateForegroundInstanceMaskRequest
が追加され、これを使用することで期待の動作が実現できるとわかりました。
背景切り抜きの実装
実際にYahoo!フリマで実装したコードを、背景切り抜き処理に絞って紹介していきます。
// 「背景を削除」ボタンを押下したとき、呼び出される
func didTapClipButton(image: UIImage) async {
do {
let resultImage = try await liftSubject(in: image)
// resultImageを元にポップアップ画面に遷移
// 遷移後の画面で「使用する」押下後、callbackでresultImageを当てはめる
} catch {
// エラーハンドリング
}
}
func liftSubject(in image: UIImage) async throws -> UIImage {
guard #available(iOS 17.0, *) else { throw CustomError() }
// UIImageをCGImageに変換する
guard let cgImage = image.cgImage else { throw CustomError() }
// 画像切り抜きに使用するリクエストをインスタンス化
let request = VNGenerateForegroundInstanceMaskRequest()
// インプット画像の画像リクエストハンドラをインスタンス化する
let handler = VNImageRequestHandler(cgImage: cgImage)
// リクエストを実行する
try handler.perform([request])
guard let result = request.results?.first else { throw CustomError() }
let mask = try result.generateMaskedImage(
ofInstances: result.allInstances,
from: handler,
croppedToInstancesExtent: false
)
let maskedImage = UIImage(ciImage: CIImage(cvPixelBuffer: mask))
// 切り抜いた画像に背景色をつける
let appliedImage = maskedImage.applyBackgroundColor(color: UIColor.white)
// 画像をリサイズ
let resizedImage = appliedImage.resize(maxImageLength: 1000)
return resizedImage
}
切り抜いた画像に背景色をつけたり、サイズ調整するためのUIImageのextensionを定義しておきます。
extension UIImage {
func applyBackgroundColor(color: UIColor) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
color.setFill()
context.fill(CGRect(origin: .zero, size: size))
let imageRect = CGRect(
x: 0,
y: 0,
width: self.size.width,
height: self.size.height
)
self.draw(in: imageRect)
}
}
func resize(maxImageLength length: CGFloat) -> UIImage {
// リサイズ処理
}
}
主要な箇所をご紹介します。
let request = VNGenerateForegroundInstanceMaskRequest()
背景から目立つオブジェクトを分離するためのインスタンスマスクを作るリクエストをインスタンス化します。
let handler = VNImageRequestHandler(cgImage: cgImage)
インプット画像の画像リクエストハンドラをインスタンス化します。インプット画像には、CGImageを指定します。
try handler.perform([request])
リクエストを実行します。Visionが画像を解析して、被写体を特定します。
let mask = try result.generateMaskedImage(
ofInstances: result.allInstances,
from: handler,
croppedToInstancesExtent: false
)
出典:Lift subjects from images in your app
インプッ ト画像から1つ以上の被写体が検出された場合、resultに切り抜いた画像のインスタンスが格納されます。今回は、ユーザーが意図的に複数の商品を撮影している可能性も考慮し、切り抜いた画像のインスタンスをすべて取り出します。また、instance0のものは含まれず、 ofInstances: [1]
のように指定して、個数を絞って取り出すことも可能です。
liftSubjectメソッド内でエラーになりうる箇所は、独自に定義したエラーを返しています。今回は、仮でCustomError()
に置き換えています。また、liftSubjectメソッドの呼び出しもとでエラーハンドリングを行うことで、処理をまとめられました。
liftSubject
を呼び出すと切り抜いた画像が返却されるため、この画像を元に差し替えを行います。
画像切り抜きの注意点
MainThreadで実行しない
リソースを消費するタスクであるため、UIをブロックしないように背景切り抜きは別スレッドで行うのが望ましいです。
画像をリサイズする
背景に色を当てるだけだと想定外のサイズになり、明るさの調整など画像編集を行った際にアプリがクラッシュするというバグが発生しました。そのための回避策として、背景色を追加し、画像をリサイズして対応しました。
おわりに
今回はYahoo!フリマiOSで出品画像の背景切り抜き機能の実装方法をご紹介しました。現在は背景色に白を固定で割り当てていますが、今後の展望として、好きな背景をユーザーが設定できるような機能の追加も検討中です。 少ないコード量で実装できるので、皆様のサービスにも取り入れやすいのではないでしょうか。この記事が少しでも皆様のお役に立てば幸いです。