0
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-21

この記事は、Pythonista3 Advent Calendar 2022 の22日目の記事です。

一方的な偏った目線で、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

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

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

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

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

オンデバイスAPI - 機械学習 - Apple Developer

Vision Framework

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

顔の情報のみを見ても:

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

たくさんありますね。

今回は、

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

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

ARKit でよくね?

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

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

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

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

実装コード

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

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

ios-vision/multi-face.png at master · googlesamples/ios-vision

格納先

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

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

大きな流れ

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

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

  • Python
    • 画像のファイルパスを取得
    • ui.View で外側の枠を準備
  • objc_util(Objective-C)
    • 画像をUIImage オブジェクトへ
      • nsurl でファイルパスをfile:// 形式に
    • UIImageView としてView に
      • 画像をイメージとして設定
      • ui.ViewaddSubview_ してもらう
    • UIImageViewlayer 領域で矩形を描画
    • 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

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 にて、解析内容は「顔」であり、その顔の位置を返してもらうようにします。

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

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

VNDetectFaceRectanglesRequest | Apple Developer Documentation

解析結果の加工

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

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

# 解析結果を取得する
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

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

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.Viewdraw メソッドがあり、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 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

書籍

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

その他

  • サンプルコード

Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。

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

  • Twitter

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

  • GitHub

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1