この記事は、Pythonista3 Advent Calendar 2022 の22日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- 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 で結果見たいおじさんなので、アプリ形式で失礼します。
画像データ
取得先
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.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
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.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 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。