Visionで実現|Swiftアプリの顔認識自動トリミングでユーザー体験向上
Swiftを使ったVisionの顔認識機能で、ユーザーのプロフィール画像を自動でトリミング。手間を省き、正確な顔検出でアプリの使いやすさを大幅改善します。
本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。
記事一覧
Vision 初探 — APPのプロフィール画像アップロードで顔を自動認識してトリミング (Swift)
Vision 実践応用
[2024/08/13 更新]
- 新しい記事、新しいAPIをご参照ください:「 iOS Vision framework x WWDC 24 Discover Swift enhancements in the Vision framework Session 」
同じく多くは語らず、まずは完成図をお見せします:
最適化前 V.S 最適化後 — 結婚吧APP
先日iOS 12がリリースされ、新たに公開されたCoreML機械学習フレームワークに注目しました。とても面白そうだと思い、現在の製品でどこに活用できるかを考え始めました。
CoreMLの初体験記事が公開されました: 機械学習を使って記事カテゴリを自動予測、モデルも自分で訓練
CoreMLは文字や画像の機械学習モデルの訓練およびAPPへの組み込みインターフェースを提供しています。私の当初の考えは、CoreMLを使って顔認識を行い、APP内でトリミング時に頭や顔が切れてしまう問題を解決することでした。上図左のように、顔が端にあると拡大+トリミングで顔が不完全になりやすいです。
ネット検索をして初めて、自分の知識不足に気づきました。この機能はiOS 11で既にリリースされている「Vision」フレームワークで、文字検出、顔検出、画像比較、QRコード検出、物体追跡などの機能をサポートしています。
ここで使用しているのは顔検出の項目であり、最適化後は右の画像のようになります。顔を検出し、それを中心にしてトリミングします。
実践開始:
まずは顔の位置をマークする機能を作り、Visionの使い方を簡単に理解しましょう
Demoアプリ
完成図は上記の通りで、写真の中の顔の位置をマークできます。
p.s 「顔」のみをマークしてください。髪の毛を含む頭全体は不可です😅
このコードは主に二つの部分に分かれています。第一部分は、画像の元のサイズをImageViewに縮小・拡大して入れたときに余白ができる問題を解決することです。簡単に言うと、画像のサイズがいくつであれば、ImageViewのサイズも同じにしたいということです。直接画像を入れると、以下のようなズレが発生します。
ContentModeを直接fill、fit、redrawに変更すると、画像が変形したり切れてしまったりすることがあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let ratio = UIScreen.main.bounds.size.width
// ここではUIImageViewの左右を0に揃え、アスペクト比を1:1に設定しているため
let sourceImage = UIImage(named: "Demo2")?.kf.resize(to: CGSize(width: ratio, height: CGFloat.leastNonzeroMagnitude), for: .aspectFill)
// KingFisherの画像リサイズ機能を使い、幅を基準に高さは自由に設定
imageView.contentMode = .redraw
// contentModeをredrawにして画像を埋める
imageView.image = sourceImage
// 画像をセット
imageViewConstraints.constant = (ratio - (sourceImage?.size.height ?? 0))
imageView.layoutIfNeeded()
imageView.sizeToFit()
// ここでimageViewのConstraintsを変更している。詳細は記事末の完全サンプルを参照
以上は画像処理に関する説明です。
トリミング部分はKingfisherを使用していますが、他のライブラリや自作の方法に置き換えることも可能です
第二部分、核心に入りコードを直接見る
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if #available(iOS 11.0, *) {
// iOS 11以降でサポート
let completionHandle: VNRequestCompletionHandler = { request, error in
if let faceObservations = request.results as? [VNFaceObservation] {
// 検出された顔
DispatchQueue.main.async {
// UIViewの操作はメインスレッドで行う
let size = self.imageView.frame.size
faceObservations.forEach({ (faceObservation) in
// 座標系の変換
let translate = CGAffineTransform.identity.scaledBy(x: size.width, y: size.height)
let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -size.height)
let transRect = faceObservation.boundingBox.applying(translate).applying(transform)
let markerView = UIView(frame: transRect)
markerView.backgroundColor = UIColor.init(red: 0/255, green: 255/255, blue: 0/255, alpha: 0.3)
self.imageView.addSubview(markerView)
})
}
} else {
print("顔が検出されませんでした")
}
}
// 検出リクエスト
let baseRequest = VNDetectFaceRectanglesRequest(completionHandler: completionHandle)
let faceHandle = VNImageRequestHandler(ciImage: ciImage, options: [:])
DispatchQueue.global().async {
// 検出は時間がかかるため、背景スレッドで実行し画面のフリーズを防ぐ
do{
try faceHandle.perform([baseRequest])
}catch{
print("エラー:\(error)")
}
}
} else {
//
print("サポートされていません")
}
主に注意すべきは座標系の変換部分です。認識された結果は画像の元の座標であり、それを外側のImageViewの実際の座標に変換して初めて正しく使用できます。
次に、本日のメインイベント — 顔の位置に基づいてプロフィール写真の正しい位置を切り抜く方法を説明します
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let ratio = UIScreen.main.bounds.size.width
// ここではUIImageViewの左右を0に揃え、アスペクト比を1:1に設定しているためです。詳細は記事末の完全サンプルを参照してください。
let sourceImage = UIImage(named: "Demo")
imageView.contentMode = .scaleAspectFill
// scaleAspectFillモードで画像を埋める
imageView.image = sourceImage
// 元の画像を直接セットし、後で操作します
if let image = sourceImage,#available(iOS 11.0, *),let ciImage = CIImage(image: image) {
let completionHandle: VNRequestCompletionHandler = { request, error in
if request.results?.count == 1,let faceObservation = request.results?.first as? VNFaceObservation {
// 顔が1つ検出された場合
let size = CGSize(width: ratio, height: ratio)
let translate = CGAffineTransform.identity.scaledBy(x: size.width, y: size.height)
let transform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -size.height)
let finalRect = faceObservation.boundingBox.applying(translate).applying(transform)
let center = CGPoint(x: (finalRect.origin.x + finalRect.width/2 - size.width/2), y: (finalRect.origin.y + finalRect.height/2 - size.height/2))
// 顔の範囲の中心点を計算
let newImage = image.kf.resize(to: size, for: .aspectFill).kf.crop(to: size, anchorOn: center)
// 中心点を基準に画像をクロップ
DispatchQueue.main.async {
// UIViewの操作はメインスレッドで行う
self.imageView.image = newImage
}
} else {
print("顔が複数検出されたか、検出されませんでした")
}
}
let baseRequest = VNDetectFaceRectanglesRequest(completionHandler: completionHandle)
let faceHandle = VNImageRequestHandler(ciImage: ciImage, options: [:])
DispatchQueue.global().async {
do{
try faceHandle.perform([baseRequest])
}catch{
print("エラー:\(error)")
}
}
} else {
print("非対応です")
}
道理は顔の位置をマークする場合とほぼ同じですが、プロフィール写真の部分は固定サイズ(例:300x300)なので、最初のImageをImageViewに適応させる部分は省略します。
もう一つの違いは、顔の範囲の中心点を計算し、その中心点を基準に画像をトリミングすることです。
赤い点は顔の範囲の中心点です
完成効果図:
頓丹前のその一秒は元の画像の位置です
完全なAPPサンプル:
コードはGithubにアップロードされています:こちらをクリック
ご質問やご意見がございましたら、お問い合わせ ください。
Post は Medium から ZMediumToMarkdown によって変換されました。
本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。





