Pythonista3 でCore ML（機械学習）入門！Vision Framework を使って画像から顔を検出する。


この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の22日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone（Pythonista3）で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

```sysInfo.log
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.1.1, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
```

他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。

ちなみに、`model iPhone12,1` は、iPhone11 です。

## この記事でわかること

- Vision Framework の画像検出の流れ
- 検出した顔情報データの取得と利用
- Pythonista3 での実装方法

## 機械学習とCore ML とVision

2022年末、機械学習は絵を描いたりコードを書いたりと、なんか凄いことになってますね。

Apple のハードウェアにはCore ML モデルとして、すでに学習されたモデルが入っています。

[Core MLの概要 - 機械学習 - Apple Developer](https://developer.apple.com/jp/machine-learning/core-ml/)

その画像認識（分類）Core ML モデルをいい感じに扱えるようにFramework が用意されています。

それがVision Frameworkです（と、いう曖昧な認識です）。

事前に端末内（iPhone）に学習モデルが入っているため、我々が学習させる必要も無いですし、大きなデータをダウンロードする必要も無いのは便利ですね。

```ざっくりイメージ
色々とある機械学習のモデル
(TensorFlow やらPyTorch のモデル)
  |
Core ML（機械学習モデルの一つ）
  | （Core ML のFramework たち）
  ├ Vision: 画像認識
  ├ Natural Language: 自然言語のテキスト分析
  ├ Speech: 言語の音声認識
  ├ Sound Analysis: 音を分析、音声タイプの認識
```

[オンデバイスAPI - 機械学習 - Apple Developer](https://developer.apple.com/jp/machine-learning/api/)

https://developer.apple.com/jp/machine-learning/api/

### Vision Framework

画像（ビデオも）から、モノを特定したり、人物をくり抜いたり、動きをTracking できます。

顔の情報のみを見ても:

- 顔検出
- フェイストラッキング
- 顔のランドマーク
- 顔のキャプチャクオリティ

たくさんありますね。

今回は、

> 画像内の人間の顔を検出します。

の「顔検出」（静止画）をPythonista3 で実装していきます。

#### ARKit でよくね？

前回、ARKit でリアルタイムに顔情報の取得をして、顔にマスクを貼り付けました。違いは何なのでしょうか？

1番の棲み分けは、リアルタイムで取得をするか、データ（画像や動画）で取得をするか？になると思います。

双方無理やり実装しようとすればできそうですが、写真や画像としてデータを持っていれば、Vision Framework の方が本来の使い方に沿った実装になると思われます。

まぁPythonista3 で実装するのであれば、趣味の域を越えることはないので、使いたい方でやりたいようにやるのがいいと思います！！！

## 実装コード

```py
from pathlib import Path
import ctypes

from objc_util import ObjCInstance, ObjCClass
from objc_util import UIImage, UIColor, UIBezierPath, NSData, nsurl, CGRect
import ui

import pdbg

VNDetectFaceRectanglesRequest = ObjCClass('VNDetectFaceRectanglesRequest')
VNImageRequestHandler = ObjCClass('VNImageRequestHandler')

UIImageView = ObjCClass('UIImageView')
CAShapeLayer = ObjCClass('CAShapeLayer')


def get_image_absolutepath(path):
  _img_path = Path(path)
  if (_img_path.exists()):
    return str(_img_path.absolute())
  else:
    print('画像が見つかりません')
    raise


def get_UIImage(path: str) -> UIImage:
  _nsurl = nsurl(get_image_absolutepath(path))
  _data = NSData.dataWithContentsOfURL_(_nsurl)
  uiImage = UIImage.alloc().initWithData_(_data)
  return uiImage


def parseCGRect(cg_rect: CGRect) -> tuple:
  origin, size = [cg_rect.origin, cg_rect.size]
  return (origin.x, origin.y, size.width, size.height)


class RectangleShapeLayer:
  def __init__(self,
               bounds: CGRect,
               frame: CGRect,
               strokeColor=None,
               fillColor=None):

    _greenColor = UIColor.greenColor().cgColor()
    _clearColor = UIColor.clearColor().CGColor()

    self.strokeColor = strokeColor if strokeColor else _greenColor
    self.fillColor = fillColor if fillColor else _clearColor

    self.layer = CAShapeLayer.alloc().init()
    self.layer.frame = bounds
    self.rect = UIBezierPath.bezierPathWithRect_(frame)

    self.path_setup()

  def path_setup(self):
    self.layer.setLineWidth_(2.0)
    self.layer.setStrokeColor_(self.strokeColor)
    self.layer.setFillColor_(self.fillColor)
    self.layer.setPath_(self.rect.CGPath())


class ViewController:
  def __init__(self, _previewView, img_path):
    self.previewView = _previewView
    self.originalImage = get_UIImage(img_path)
    self.imageView = UIImageView.alloc().initWithImage_(self.originalImage)
    self.overlayLayer = CAShapeLayer.alloc().init()
    self.setupOverlay()
    self.faceDetection()

    self.previewView.addSubview_(self.imageView)

  def setupOverlay(self):
    self.overlayLayer.frame = self.imageView.bounds()
    self.imageView.layer().addSublayer_(self.overlayLayer)

  def faceDetection(self):
    cgImage = self.originalImage.CGImage()
    request = VNDetectFaceRectanglesRequest.alloc().init().autorelease()
    handler = VNImageRequestHandler.alloc().initWithCGImage_options_(
      cgImage, None).autorelease()

    handler.performRequests_error_([request], None)
    observation_array = request.results()
    self.drawFaceRectangle_observations_(observation_array)

  def drawFaceRectangle_observations_(self, observations):
    bounds = self.overlayLayer.frame()
    _, _, layerWidth, layerHeight = parseCGRect(bounds)

    for observation in observations:
      _x, _y, _width, _height = parseCGRect(observation.boundingBox())

      width = _width * layerWidth
      height = _height * layerHeight
      x = _x * layerWidth
      # todo: 左下原点から、左上原点へ
      y = (layerHeight - height) - (_y * layerHeight)
      frame = ((x, y), (width, height))

      rect = RectangleShapeLayer(bounds, frame)
      self.overlayLayer.addSublayer_(rect.layer)


class View(ui.View):
  def __init__(self, img_path, *args, **kwargs):
    ui.View.__init__(self, *args, **kwargs)
    self.bg_color = 'maroon'
    self.view_controller = ViewController(self.objc_instance, img_path)


if __name__ == '__main__':
  img_file_path = './img/multi-face.png'

  view = View(img_file_path)
  view.present(style='fullscreen', orientations=['portrait'])

```

機械学習を使った実装は、console 出力で処理部分をミニマムに説明。の方が、無駄がなく断然いいのですが、、、

View で結果見たいおじさんなので、アプリ形式で失礼します。

### 画像データ

#### 取得先

[googlesamples/ios-vision](https://github.com/googlesamples/ios-vision)

https://github.com/googlesamples/ios-vision

Google 先生のVision サンプルリポジトリにちょうどいいのがあったのでお借りします。

[ios-vision/multi-face.png at master · googlesamples/ios-vision](https://github.com/googlesamples/ios-vision/blob/master/FaceDetectorDemo/FaceDetector/multi-face.png)

https://github.com/googlesamples/ios-vision/blob/master/FaceDetectorDemo/FaceDetector/multi-face.png

#### 格納先

実行するファイルのディレクトリに`img/` フォルダを作成し`multi-face.png` を格納しています。

```
.
├── img
│   └── multi-face.png
└── 実行するファイル.py
```

### 大きな流れ

基本的に、Vision Framework でメインどころはよしなにやってくれています。

メインどころ前後のコードで書いている部分を見ていきます。

- Python
  - 画像のファイルパスを取得
  - `ui.View` で外側の枠を準備
- `objc_util`（Objective-C）
  - 画像を`UIImage` オブジェクトへ
    - `nsurl` でファイルパスを`file://` 形式に
  - `UIImageView` としてView に
    - 画像をイメージとして設定
    - `ui.View` に`addSubview_` してもらう
  - `UIImageView` の`layer` 領域で矩形を描画
  - `VNImageRequestHandler` に画像を指定
    - `VNDetectFaceRectanglesRequest` として「顔を見つけてら矩形で返して」とお願い（Request）
  - `layer` 領域に見つけた画像内の位置データを描画

#### `UIImage` -> `UIImageView` ->（`objc_util`）-> `ui.View`

何よりもまず、Python（Pythonista3）の処理と、`objc_util` で処理したオブジェクト同士の連携が必要ですね。

（今回は）`ui.View` のView はあくまでも、Pythonista3 上で表示させるものとして考えます。

[ui.imageview | ui — Native GUI for iOS — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/ui.html#imageview)

https://omz-software.com/pythonista/docs/ios/ui.html#imageview

`ui.imageview` で画像を入れ、`ui.imageview.objc_instance` にて`objc_util` へブリッジしても問題はありません。

しかし、連携をするにあたり役割分担を明確にしたいため、`UIImage` として、画像を`objc_util` のオブジェクトとします。

`objc_util`（Objective-C）のView として所持するために、`UIImageView` でView を作成します。

そうすることで、Pythonista3側の結果を表示させる`ui.View` は、`.objc_instance.addSubview_` 処理のみに注力することができます。

`ui.View.objc_instance.addSubview_` する以前の処理を好きなだけガチャガチャとできるわけです。

（`objc_util` 処理で頑張る必要がありますが。。。）

#### Vision Framework 周りの処理

`ViewController.faceDetection` メソッド内が、顔検出の設定と処理を司っています。

`VNImageRequestHandler` で、指定した画像を解析する準備をします。

`VNDetectFaceRectanglesRequest` にて、解析内容は「顔」であり、その顔の位置を返してもらうようにします。

今回ももれなく、エラーハンドリングを行なっていません😇

```py
def faceDetection(self):
  # VNImageRequestHandler へ取り込めるイメージオブジェクトを呼び出す
  cgImage = self.originalImage.CGImage()

  # 顔の検出をして、その位置を教えてもらうようにする
  request = VNDetectFaceRectanglesRequest.alloc().init().autorelease()

  # イメージオブジェクトを画像解析できるようにする
  handler = VNImageRequestHandler.alloc().initWithCGImage_options_(
    cgImage, None).autorelease()

  # 解析内容は「顔」であると教えて、解析をはじめる
  handler.performRequests_error_([request], None)

  # 解析結果を取得する
  observation_array = request.results()

  # 解析結果を使いたいように加工する
  self.drawFaceRectangle_observations_(observation_array)

```

[VNImageRequestHandler | Apple Developer Documentation](https://developer.apple.com/documentation/vision/vnimagerequesthandler?language=objc)

https://developer.apple.com/documentation/vision/vnimagerequesthandler?language=objc

[VNDetectFaceRectanglesRequest | Apple Developer Documentation](https://developer.apple.com/documentation/vision/vndetectfacerectanglesrequest?language=objc)

https://developer.apple.com/documentation/vision/vndetectfacerectanglesrequest?language=objc

#### 解析結果の加工

`ViewController.drawFaceRectangle_observations_` メソッドで、View 上に矩形を出すまで処理をしています。

`hoge_fuga_` と`最後に_`がありますが、今回作成した独自のメソッドです。

```py
# 解析結果を取得する
observation_array = request.results()

# 解析結果を使いたいように加工する
self.drawFaceRectangle_observations_(observation_array)
```

`observation_array` に配列で`VNFaceObservation: boundingBox=[横位置x, 縦位置y, 幅width, 高さheight]` として入っています。

注意点としては、`boundingBox` の情報が

- `0.0 〜 1.0` の相対的な値
- 座標原点`(0, 0)` が左下

UIView は左上原点なので、値を変換する必要があります。

```
UIView
(0, 0)  (1, 0)
  ┏------┐
  |      |
  |      |
  └------┘
(0, 1)  (1, 1)


VNFaceObservation: boundingBox
(0, 1)  (1, 1)
  ┌------┐
  |      |
  |      |
  ┗------┘
(0, 0)  (1, 0)

```

[【iOSアプリ開発】Viewの座標やサイズを取得するノウハウ - Qiita](https://qiita.com/st43/items/3802624d15a8dded8169)

https://qiita.com/st43/items/3802624d15a8dded8169

`for` で回し、各顔情報の矩形を作成します。

```py
def parseCGRect(cg_rect: CGRect) -> tuple:
  # CGRect 情報を力技で展開し、Python 上で扱いやすく
  origin, size = [cg_rect.origin, cg_rect.size]
  return (origin.x, origin.y, size.width, size.height)



def drawFaceRectangle_observations_(self, observations):
  # 画像サイズになっているView 内のLayer サイズを取得
  bounds = self.overlayLayer.frame()
  # 横幅と縦幅のみ保持
  _, _, layerWidth, layerHeight = parseCGRect(bounds)

  for observation in observations:
    # ひとつひとつの顔の位置とサイズ情報を操作
    _x, _y, _width, _height = parseCGRect(observation.boundingBox())

    width = _width * layerWidth
    height = _height * layerHeight
    x = _x * layerWidth
    # todo: 左下原点から、左上原点へ
    y = (layerHeight - height) - (_y * layerHeight)
    frame = ((x, y), (width, height))

    # 親とするself.overlayLayer へ矩形をつくり重ねていく
    rect = RectangleShapeLayer(bounds, frame)
    self.overlayLayer.addSublayer_(rect.layer)
```

#### `UIBezierPath` で矩形を描く

`ui.View` に`draw` メソッドがあり、View 上に図形を描いたり、Path で線を引くことができました。

もちろんUIView でも同じことができます（`ui.View` がUIView のラッパーなので、そりゃそうやろ。。。ですね）。

View の`CAShapeLayer` へ、お絵描きしていくことになります。

`CAShapeLayer` も`.addSublayer_` として、他のLayer を重ねることができます。

描き方は`ui.View.draw` の流れとほぼ同じです。呼び出しに関して`objc_util` の手間が一つ二つ多いくらいです。

- 線や面の塗りたい色を決める
- 線の太さを決める
- 描画したいパスを呼ぶ

今回は、親のLayer サイズ分確保して、再度子のLayer をつくり、その中に矩形を描いているのでかなり無駄な処理をしてしまっていますね。。。

描画できればヨシ！とさせてください。。。

## 次回は

Vision Framework を使い、画像の顔検出ができました。

事前に学習済みのモデルがあるので、呼び出す方法だけを考えればいいので気が楽ですね。

`VNDetectFaceRectanglesRequest` に焦点を置いていたので、`UIImageView` まわり等かなりザルになってしまっています。

サンプル画像もちょうど収まるサイズでしたので、View 側での調整を行っていません。

大きい画像の場合、しれっとView に見切れる状態になるので調整チャレンジをしてみるのも面白いかもしれません。

検出した値情報が、`0.0 〜 1.0` で帰ってくるので、その点は便利ですね。

一枚の静止画を検出したとしたら、次はやはり連続した複数の静止画（動画）検出をやりたくなりますね。

次回は、カメラから取ったリアルタイムの情報から、「手」を検出して追いかけられるようにします。

ここまで、読んでいただきありがとうございました。

## せんでん

### Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC

### 書籍

iPhone/iPad でプログラミングする最強の本。

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

https://github.com/pome-ta