【Swift】iOSアプリで画像認識をする手段（Visionの活用）

# 目次
１、はじめに（執筆した理由、結論が端的に書いてあります。）
２、iOSアプリで画像認識をする手段
３、CreateMLでモデル作ってみた！
４、Visionだけで手を識別してみた！
５、結論

# １、 はじめに
この記事は5分で読めます。
初投稿のため、分かりにくい部分&間違ってる部分等ありましたら教えていただけると幸いです。

分かりやすさ重視でざっくりとした内容なので、具体的なやり方は書いてありません。あくまで、技術選定の部分にフォーカスしております。

個人開発で「画像認識を用いた後出しジャンケンアプリ」を開発しました。
開発をする上で、「画像認識」の技術選定に手間取ったため記事を書きました。
結論としては、Vision frameworkのみでじゃんけんの手を識別しました。

# ２、 iOSアプリで画像認識をする手段

### iOSアプリ開発で画像認識をする手段の一例です。
①Appleの公式サンプルモデルを使用して画像認識
②CreateMLでモデルを作成してから画像認識
③Pythonとかでモデルを作成してから画像認識
④Vision frameworkのみで画像認識

### それぞれの選び方
まず、①Appleの公式サンプルモデルにやってみたいモデルがないか確認しましょう。
もしある場合は、この記事の役目はここまでです。

Appleの公式サンプルモデルにやってみたいモデルがなかった場合、モデルを自作する必要があります。
モデルは、②CreateMLとか③Pythonで作れます。（他にもあると思いますが、、）

最後に、Vision frameworkのみで画像認識をする方法もあります。

ちなみに筆者は以下のような流れで開発しました。
①じゃんけんを識別する公式サンプルモデルがなかった
↓
②CreateMLでモデルを作ってみた！→うまくいかない
↓
③Vision frameworkだけで手を識別してみた！→うまくいった！

# ３、 CreateMLでモデルを作ってみた！
### CreateMLでモデルを作る手順と注意点
CreateMLでモデルを作る手順は主に２つです。
１、識別したい写真をクラス毎にいっぱい撮影する。
２、CreateMLでトレーニングする

まず、クラス毎に写真を撮影します。このとき、量が大切だと思ってとにかく沢山撮るとうまくいきません。質も大切です。質とは、背景の単色化をしたり、あえて画質の悪いものを撮影する、識別するものを様々な角度から撮影するなどです。量は識別したいものによって異なります。

例えば、「机」と「椅子」を識別する場合は、全く見た目が異なるので、そこまで量は必要ありません。（各１００枚程度でもうまく識別できるかも）しかし、「グー」と「チョキ」と「パー」のように"同じものの異なる状態"の場合にはかなりの量が必要です。（各１０００枚でも足りないかも）

次にCreateMLでトレーニングをします。CreateMLではクラス毎のファイルを突っ込んでトレーニングボタンを押すとモデルを作ってくれます。詳しくは他の記事を参考にしてください。

### じゃんけんの手のモデルをCreateMLで作る
じゃんけんの手は"同じもの（手）の異なる状態"なので、グーチョキパーそれぞれ１０００枚以上撮影しました。さらに、何も写っていないクラスも別途用意しました。これらを用いてモデルを作りましたが識別率は１０%以下でした。。。

### CreateMLが適している場面
CreateMLなどの、パターン認識では、同じものの異なる状態を識別するのは難しいです。大量に撮影すればいけるけど、、、、
なので、CreateMLは「見た目が全く異なるもの」を識別するのに向いています。

# ４、 Visionだけで手を識別してみた！

### Vision frameworkで手を識別する方法
Vision frameworkでは、顔、体、手の「座標データ」が取得できます。他にもいろいろできますが、座標データを使用するのでVision frameworkでできることの詳細は省きます。座標データを取得したら、位置関係（角度、長さ）を求めます。この位置関係の情報を用いて識別します。

### じゃんけんの手をVisionで識別
① 親指以外の「指先、第二関節、手首」の座標データを取得します。
```swift
// 指先
let indexTip = points[VNHumanHandPoseObservation.JointName.indexTip.rawValue]?.location ?? .zero
let middleTip = points[VNHumanHandPoseObservation.JointName.middleTip.rawValue]?.location ?? .zero
let ringTip = points[VNHumanHandPoseObservation.JointName.ringTip.rawValue]?.location ?? .zero
let littleTip = points[VNHumanHandPoseObservation.JointName.littleTip.rawValue]?.location ?? .zero

// 近位指節間(PIP)関節 ＝ 第二関節のこと
let indexPIP = points[VNHumanHandPoseObservation.JointName.indexPIP.rawValue]?.location ?? .zero
let middlePIP = points[VNHumanHandPoseObservation.JointName.middlePIP.rawValue]?.location ?? .zero
let ringPIP = points[VNHumanHandPoseObservation.JointName.ringPIP.rawValue]?.location ?? .zero
let littlePIP = points[VNHumanHandPoseObservation.JointName.littlePIP.rawValue]?.location ?? .zero

// 手首
let wrist = points[VNHumanHandPoseObservation.JointName.wrist.rawValue]?.location ?? .zero
```
② 「手首から指先までの長さ」「手首から第二関節までの長さ」を算出
```swift
// 手首から指先の長さ
let wristToIndexTip = distance(from: wrist, to: indexTip)
let wristToMiddleTip = distance(from: wrist, to: middleTip)
let wristToRingTip = distance(from: wrist, to: ringTip)
let wristToLittleTip = distance(from: wrist, to: littleTip)

// 手首から近位指節間(PIP)関節の長さ
let wristToIndexPIP = distance(from: wrist, to: indexPIP)
let wristToMiddlePIP = distance(from: wrist, to: middlePIP)
let wristToRingPIP = distance(from: wrist, to: ringPIP)
let wristToLittlePIP = distance(from: wrist, to: littlePIP)
```

ちなみにdistanceは以下のように三平方の定理を用いて求めます。↓
```swift
// 画面上の２点間の距離を三平方の定理より求める
private func distance(from: CGPoint, to: CGPoint) -> CGFloat {
    return sqrt(pow(from.x - to.x, 2) + pow(from.y - to.y, 2))
}
```
③ それぞれの指で②の２つの長さを比較して、指が曲がっているか判定
「手首から指先までの長さ」より「手首から第二関節までの長さ」が長いとその指は曲がっています。
```swift
wristToIndexTip < wristToIndexPIP
```
④ 曲がっている指によってグーチョキパーの識別
```swift
// HandPoseの判定(どの指が曲がっているかでグーチョキパーを判定する）
if
wristToIndexTip > wristToIndexPIP &&
wristToMiddleTip > wristToMiddlePIP &&
wristToRingTip > wristToRingPIP &&
wristToLittleTip > wristToLittlePIP {
    // ４本の指が曲がっていないのでぱー
    currentGesture = .paper
} else if
wristToIndexTip < wristToIndexPIP &&
wristToMiddleTip < wristToMiddlePIP &&
wristToRingTip < wristToRingPIP &&
wristToLittleTip < wristToLittlePIP {
    // ４本の指が曲がっているのでぐー
    currentGesture = .rock
} else if
wristToIndexTip > wristToIndexPIP &&
wristToMiddleTip > wristToMiddlePIP {
    // IndexとMiddleが曲がっていないのでちょき
    currentGesture = .scissors
} else {
    currentGesture = .unknown
}
```
結果は、識別率90%以上という結果になりました。
### Vision frameworkのみでの識別が向いている場面
じゃんけんの手の識別のように、座標データから簡単に識別できる場合や座標の変化量を使用したい場合に適しています。座標の変化量を使用する例としては、「姿勢管理アプリで体がどれくらい曲がっているか」などです。
# ５、 結論
今回、じゃんけんアプリでは④のVision frameworkのみで手を識別しました。しかし、他にももっといい方法があるかもしれないので、あくまで参考程度にしてください。
じゃんけんアプリのコードは以下からご覧になれます。
[画像認識を用いたじゃんけんアプリのコード](https://github.com/ryuprogrammer/CheatingJanken)

最後まで読んでいただきありがとうございました。もし間違っている部分等あれば教えていただけると幸いです。