iOS14で Vision Framework に追加されたオプティカルフローを使って 「呼吸(の様子)」 をすこし解析しやすいように可視化してみました。
<完成イメージ>
お腹の膨らみ・引っ込みの向き・量を把握することで呼吸の数をカウントすることがゴールです(今回は呼吸の可視化まで。呼吸数のカウントは次の記事に記載予定)。
この記事で役に立つかもしれない情報
- Vision Frameworkのオプティカルフローでできることは何?
→ 時系列の前後の画像からピクセル毎 にピクセルの移動量x, yを取得でできます。
→ 移動量 x, y はCVPixelBuffer
内にFloat
型で交互に格納されてます。 - CIFilterで独自のフィルタをMSL(Metal Shader Language)で作る方法は?
→ 後述
1. オプティカルフロー
1−1. オプティカルフローとは
「視覚表現(通常、時間的に連続するデジタル画像)の中で物体の動きをベクトルで表したもの」(Wikipediaより)
この記事で紹介する Vision Framework のオプティカルフローは、ピクセル毎に移動量x, yを導出するというもの。
下の動画は、左側が「移動したピクセル(移動量が大きいピクセル)」が確認できる例で、右側が「ピクセルが移動した方向」が確認できる例。
移動したピクセルを表示 | ピクセルがどちらに移動したのか表示 |
---|---|
![]() |
![]() |
移動前後の画像を与えることでピクセル毎に移動量を取得することができ、上の動画の例では撮影フレーム毎に前後の移動量を可視化してます。
画像解析で用いられるOpenCV のオプティカルフロー(Optical Flow)のドキュメント
を見るとオプティカルフローのアルゴリズムにはいくつかあるようですが、Vision FrameworkのアルゴリズムがなんなのかはAppleのドキュメントから見つけられませんでした。
1−2. VNGenerateOpticalFlowRequest
そではどうやってオプティカルフローを作るかというと、とても簡単で次のコードだけです。
var requestHandler = VNSequenceRequestHandler()
let request = VNGenerateOpticalFlowRequest(targetedCGImage: newImage, options: [:])
try self.requestHandler.perform([request], on: baseImage)
if let pixelBufferObservation = request.results?.first as? VNPixelBufferObservation {
result = CIImage(cvImageBuffer: pixelBufferObservation.pixelBuffer)
}
VNGenerateOpticalFlowRequest
がオプティカルフローを取得するためのリクエストで、このサンプルでは時系列的に後になる画像を targetedCGImage
に指定してます。
次に VNSequenceRequestHandler
の perform()
に上記のリクエストと 時系列で前側になる画像を on
に指定してます(ちなみに、VNSequenceRequestHandler の代わりに VNImageRequestHandler
を使っても動作します)。
あとはオプティカルフローを受け取るだけ。
request.results?.first
(CVPixelBuffer)の中に 連続する x, y の移動量がFloat型で格納されます。
注意点としては、与える2枚の画像のサイズが一致していないと例外が発生します。
2. オプティカルフローの可視化
2−1. CIImageの挙動について
先のコードではピクセル毎のベクトル情報が入ったCVPixelBuffer を与えてCIImageをインスタンス化してます。
この記事のサンプルコードは WWDC2020の『Explore Computer Vision APIs』を参考にしており、その中でこうやってCIImageをインスタンス化しているのですが「画像情報」ではなく「移動量」という情報をCIImageに与えていいの?移動量は1を大きく超えるしマイナスになることもあるので、色としてそのまま使えないからダメでは?思いました。
で、インスタンス化したCIImageがどうなっているのか中を見ると次のようになってます。
(lldb) po result
▿ Optional<CIImage>
- some : <CIImage: 0x283a801a0 extent [0 0 480 640]>
affine [1 0 0 -1 0 640] extent=[0 0 480 640] opaque
colormatch sRGB_to_workingspace extent=[0 0 480 640] opaque
IOSurface 0x283a9bae0(1011) seed:1 RGf alpha_one extent=[0 0 480 640] opaque
ポイントとなるのはどうやら RGf
の部分で、これはピクセルフォーマットのことでCIFormat
.RGf で定義されており「1ピクセルあたり64ビットであり、RとGの値をFloatで持つ」とあります。つまりFloatのxがR、yがGに格納されている、と解釈でき、型がなにか別ものに変わっておかしなデータとして扱われる、、、という心配は減りました。また、CIImageはAppleのドキュメントに次のように記載されており、投入したデータが何もせず加工されてしまうということはなさそうです。
Although a CIImage object has image data associated with it, it is not an image. You can think of a CIImage object as an image “recipe.” A CIImage object has all the information necessary to produce an image, but Core Image doesn’t actually render an image until it is told to do so.
(Google翻訳)CIImageオブジェクトには画像データが関連付けられていますが、画像ではありません。 CIImageオブジェクトは、画像の「レシピ」と考えることができます。 CIImageオブジェクトには、画像を生成するために必要なすべての情報が含まれていますが、Core Imageは、指示されるまで実際には画像をレンダリングしません。
ちなみに、移動量が入った CVPixelBuffer のデータの単位が「Float32が2つ」という情報はどこから来たのかというと、CVPixelBufferGetPixelFormatType
を使ってCVPixelBufferを調べてみるとわかります。
let ostype = CVPixelBufferGetPixelFormatType(pixelBufferObservation.pixelBuffer)
CVPixelBufferGetPixelFormatTypeを使うことで CVPixelBuffer のフォーマットを取得することができ kCVPixelFormatType_TwoComponent32Float となっていたのでFloat32が2つ分を扱う定義となっていました(OSTypeの見方はややこしく、取得したUInt32を8bit毎にASCIIにする。今回確認した値は0x32433066 -> "2C0F")。
CIImageの中で値は期待通りFloat32が2つを1単位として管理できているようなので、次に、このCIImageをそのままUIImageViewで表示するどうなるのか確認してみました。
ペンを左から右に移動 | ペンを右から左に移動 |
---|---|
![]() |
![]() |
すこしわかりづらいですが、ペンを左から右に移動したときはx方向(Red)の移動がプラスなのでペンは赤くなり、右から左に移動したときはマイナスなので黒くなっており、つまり、CIImage(なのかUIImageViewなのか)は表示できないような大きな値は無視するだけ、という動作です(そもそも、RGf
のようにFloat32を扱えるフォーマットを用意しているので想定の動作、ということなのですね。そういえばMetalのフラグメントシェーダーの戻り値もfloat4やhalf4なので、GPUレベルで取り扱えるということ)。
2−2. CIKernelによる独自フィルタの作成
この記事ではオプティカルフローの可視化のため、CIKernelで三角形でフローの向きを表示する独自フィルタを実装しています。
CIKernelによる独自フィルタの作り方についてswift部分は参考にさせていただいたこちらの記事『Core ImageのカスタムフィルタをMetalで書く』のまんまです。
以下、本サンプルでの設定です。
Xcodeの設定
Xcodeの設定はAppleのドキュメントが更新されていないらしく、次のように設定する必要がありました(Xcode12.5で確認)。
MTLLINKER_FLAGSはドキュメントだと-cikernel
ですがワーニングがでるので指示通り修正します。
MSL
この記事で使っている三角形でフローの向きを表示する独自フィルタはWWDC2020の『Explore Computer Vision APIs』で紹介されているコードをベースにしています。
注) WWDCのサンプルコードはブラウザからは参照できません。Apple Developerアプリからビデオをみると「コード」タブから参照できます。
ちょっと残念なことにWWDCのサンプルコードはMSLではなくCore Image Kernel Languageで書かれています(なんで?)。MSLへの移植は容易なのですが、1点躓いたところがあります。
Core Image Kernel Languageだと描画先の座標は destCoord() で取得できますが、MSLではこの方法では取得できませんでした。色を決める関数の引数にdestination
型の引数を指定することで、その型のcoord()
から取得することができます。ちなみに、swift側からは引数を与える必要はありません。
extern "C" {
namespace coreimage {
half4 flowView(sampler_h image, destination dest)
{
// 描画先の座標
float2 destCoord = dest.coord();
// 略
この辺りの仕様は『Metal Shading Language for Core Image Kernels』に記載されています。
その他MSLで三角形作る方法について興味がある方はGithubにソースをあげているので参照ください。
2−3. 比較する画像の決定
呼吸の振幅を確認するため、基点となる画像を決めて、基点の画像と各時点の画像を比較することで移動量の変化を確認します。
今回作成したサンプルは「観察開始」ボタンタップ時の画像を基点としています。
最後に
iOS+オプティカルフローで何ができそうなのか?
ググったり、精一杯思いついたものとして。。。
- 膨らんだり萎んだり、振動したりするものの観察 例)生体のイメージング。
本記事はもともとはこちらの論文『 オプティカルフローによる脈波検出』のように脈をとって心電図っぽくしたかったが、自分の手首をiPhoneで撮影しても全く変化がなかったので「呼吸」を採用。 - 何か動いたことの検出 例)だるまさんが転んだ、G検出機、防犯
- どのくらいの移動があるのか、量の把握 例)交通量(?)、人混み(?)
次の記事で呼吸の数を数えてみます。