- 投稿日:2019-08-11T23:13:42+09:00
Vision Frameworkを使ってリアルタイムでユーザーの顔をトラッキングしてみた
iOSアプリに顔認識を導入しようと思うと、昔ならOpenCVやTensorFlowでモデルをつくって…というようにいささかハードルが高く感じられたものですが、AppleさんがVisionフレームワークを用意してくれたおかけで、そのハードルは大きく下がりました。
今回、Appleのサンプルコードを使いながら、リアルタイムでユーザーの顔をトラッキングしてみました。
できるアプリ
このように顔の中の特徴的な部分(ランドマーク)を勝手に検出し、Bounding Box(境界ボックス)で囲ってくれます!
境界ボックスは、顔が移動すると追跡してくれるので、境界ボックスの座標をもとにして、顔が画面のどこに寄っているかなどもとることができます。サンプルコード
https://developer.apple.com/documentation/vision/tracking_the_user_s_face_in_real_time
このページのDownloadボタンからダウンロードして、実機でビルドすると顔認識アプリができます。解説
サンプルコードの239行目あたりの
let faceDetectionRequest = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in if error != nil { print("FaceDetection error: \(String(describing: error)).") } guard let faceDetectionRequest = request as? VNDetectFaceRectanglesRequest, let results = faceDetectionRequest.results as? [VNFaceObservation] else { return } DispatchQueue.main.async { // Add the observations to the tracking list for observation in results { let faceTrackingRequest = VNTrackObjectRequest(detectedObjectObservation: observation) requests.append(faceTrackingRequest) } self.trackingRequests = requests } })で、検出された顔の解析が行われています。
ここの
for observation in results { let faceTrackingRequest = VNTrackObjectRequest(detectedObjectObservation: observation) requests.append(faceTrackingRequest) }のobservationから顔のランドマークやロール、ヨー角など様々な情報にアクセスできます。
たとえば、
for observation in results { let faceTrackingRequest = VNTrackObjectRequest(detectedObjectObservation: observation) requests.append(faceTrackingRequest) // 顔のロールの取得 print(observation.roll) }のようなイメージです。
ついでに境界ボックスから顔が画面のどこに寄っているかもとってみましょう。
サンプルコードの554行目あたりの
guard let observation = trackingResults[0] as? VNDetectedObjectObservation else { return }で、トラッキングしたユーザーの顔の結果を取得しているので、この
observation
を使って、境界ボックスの座標を取得します。if observation.boundingBox.maxX >= 0.9 { print("顔が画面の右に寄っています") }上のように書くことで境界ボックスのX座標の画面からの比率を取得することができます。(画面の右端が1です)
今回は境界ボックスのX座標が0.9以上で画面の右に寄っていると出力するようにしてみました。
まとめ
Appleの機械学習で良いイメージはあまりなかったのですが、Visionフレームワークは予想以上に便利だなと思いました。
ただ、動画内でのまばたきや笑顔の検出はVisionだけでは厳しそうなので、Amazon Rekognition Videoなどを使う必要がありそうです。
とはいっても予想以上に面白かったので、これからもCore MLやCreate MLなどで遊んでみたいと思います。
- 投稿日:2019-08-11T21:41:04+09:00
【Swift】XMLParserについて
XMLParserについてまとめました。
Yahoo! RSS( https://news.yahoo.co.jp/pickup/rss.xml )を使用して、最新の記事の情報を取得します。解析するXMLについて
Yahoo! RSSのXMLは以下のようになっています。
<rss xmlns:blogChannel="http://backend.userland.com/blogChannelModule" version="2.0"> <channel> <title>Yahoo!ニュース・トピックス - 主要</title> <link>https://news.yahoo.co.jp/</link> <description>Yahoo! JAPANのニュース・トピックスで取り上げている最新の見出しを提供しています。</description> <language>ja</language> <pubDate>Sun, 11 Aug 2019 02:17:25 +0900</pubDate> <item> <title>記事のタイトル</title> <link>記事のURL(Link)</link> <pubDate>配信時間</pubDate> <enclosure length="133" url="https://s.yimg.jp/images/icon/photo.gif" type="image/gif"> </enclosure> <guid isPermaLink="false">記事のURL(PermaLink)</guid> </item> <item>・・・</item> </channel> </rss>今回は、記事のタイトルとURLの情報がほしいため、< title >と< link >の要素を取得します。
XMLの取得
ViewControllerにXMLParserDelegateプロトコルを実装させます。
class ViewController: UIViewController, XMLParserDelegate, UITableViewDataSource, UITableViewDelegate { }URLSessionを使用し、XMLをDataで取得します。
取得後にDataの中身を解析します。let url: URL = URL(string:"https://news.yahoo.co.jp/pickup/rss.xml")! let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in let parser: XMLParser? = XMLParser(data: data!) parser!.delegate = self parser!.parse() }) //タスク開始 task.resume()XMLParserの解析処理
要素の取得時に呼び出される関数から、記事のタイトル等の値を取得します。
var check_title = [String]() var news_title = [String]() var link = [String]() var enclosure = [String]() var check_element = String() //解析_開始時 func parserDidStartDocument(_ parser: XMLParser) { } //解析_要素の開始時 func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { if elementName == "enclosure" { enclosure.append(attributeDict["url"]!) } check_element = elementName } //解析_要素内の値取得 func parser(_ parser: XMLParser, foundCharacters string: String) { if string != "\n" { if check_element == "title" { //要素がtitleの場合値を取得する check_title.append(string) } if check_element == "link" { //要素がlinkの場合値を取得する link.append(string) } } } //解析_要素の終了時 func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { if check_element == "title" { var title = check_title[0] for i in 1..<check_title.count { //titleの値が複数取得された場合一つにまとめる title = title + check_title[i] } check_title = [String]() news_title.append(title) } } //解析_終了時 func parserDidEndDocument(_ parser: XMLParser) { ![undefined]() } //解析_エラー発生時 func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { print("エラー:" + parseError.localizedDescription) }実行結果
XMLParserで取得した値から記事の一覧を表示しています。
タイトルを選択するとUIWebViewで記事の内容を表示します。参考
https://developer.apple.com/documentation/foundation/xmlparserdelegate
https://mizumotok.hatenablog.jp/entry/2018/04/13/001002
- 投稿日:2019-08-11T20:43:39+09:00
SwiftでAPIを用いる流れ(ライブラリを使わないversion)
iOSアプリにおいて、 AlamofireやSwiftyJSONなどのライブラリを用いずにAPIを利用する流れについてまとめてみました。
使用するもの
- Codable(https://qiita.com/UJIPOID/items/2c436a80f1167f7bcac0)
- Qiita API(https://qiita.com/api/v2/items)
- コアライブラリFondation(URL、URLRequest、URLResponse、URLSessionクラスを用いる)
やること
TableViewにAPIから取得したデータを一覧で表示する。
サーバーにHTTPリクエストを送り、返ってきたHTTPリクエストを受け取ってTableViewに表示する。手順
①Codableを使ってJSONを受け取る構造体(モデル)を作る。
QiitaStruct.swiftstruct QiitaStruct: Codable { var title: String var user: User struct User: Codable { var name: String } }②HTTPリクエストの生成
URLオブジェクトの生成
QiitaViewController.swiftlet url = "https://qiita.com/api/v2/items"URLRequestオブジェクトの生成
QiitaViewController.swiftlet request = URLRequest(url: url)③HTTPリクエストを投げ、返ってきたレスポンスを受け取って配列に格納する。
URLSessionクラスのインスタンスからリクエストを送り、その後のデータを受け取った後の処理について記述する。
QiitaViewController.swiftlet qiitaArray:[QiitaStruct] = [] let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in guard let jsonData = data else { return } do { let articles = try JSONDecoder().decode([QiitaStruct].self, from: jsonData) qiitaArray.append(articles) } catch { print(error.localizedDescription) } } task.resume()④格納した配列の値をTableViewに表示する。
QiitaViewController.swiftextension QiitaViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell") let article = articles[indexPath.row] cell.textLabel?.text = article.title cell.detailTextLabel?.text = article.user.name return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return articles.count } }以上の手順により、以下のようにTableViewにデータを表示することができました。
作成したQiitaAPIのサンプルプロジェクトは以下のGithubページからクローンできます。
サンプルプロジェクトでは、MVVM設計でコンポーネントを作って、少しだけ実装を抽象化しています。
https://github.com/YukiNagai1016/QiitaAPI
- 投稿日:2019-08-11T16:49:03+09:00
FlutterでネイティブのViewをそのまま表示できるPlatformViewを使う方法
FlutterのPlatformViewという
ネイティブのViewをFlutterのWidgetのように表示できる
というものです。自分のプロジェクトでは画像の加工をよくやるので、結構使いそうです。
メモとして残します。今回はiOSだけ紹介しますが、Androidも基本的には変わりません。
Androidでやる場合はこちら → https://medium.com/flutter-community/flutter-platformview-how-to-create-flutter-widgets-from-native-views-366e378115b6
info.plist
を編集する下記を
info.plist
を追加しましょう。<key>io.flutter.embedded_views_preview</key> <true/>Flutter側を編集する
Flutter側はこれだけです。
UiKitView( viewType: "ramdom_noise",),iOS側
iOS側はAppDelegate.swiftを使います。
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { let viewFactory = FluffViewFactory() registrar(forPlugin: "kitty").register(viewFactory, withId: "ramdom_noise") GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } public class FluffViewFactory: NSObject, FlutterPlatformViewFactory { public func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any? ) -> FlutterPlatformView { return FluffView(frame, viewId: viewId, args: args) } } public class FluffView : NSObject, FlutterPlatformView { let frame: CGRect let viewId: Int64 init(_ frame: CGRect, viewId: Int64, args: Any?) { self.frame = frame self.viewId = viewId } public func view() -> UIView { return UISlider(frame: frame) } }これで画面に Sliderが表示されます。
参考
- 投稿日:2019-08-11T14:47:03+09:00
[Java / Swift]Java Interface と Swift Protocol の比較
この記事で書くこと
Java
interface
と SwiftProtocol
の比較この記事を書いた理由
・ 基本情技術者試験対策
・ Java と Swift で似ている箇所の比較コード例(Java interface)
main.javapublic class Main { interface sampleInterface{ String sampleFunction(String a, String b); } public static void main(String[] args) throws Exception { /* クラスに interface を実装する時は、implements と書く */ class A implements sampleInterface{ public String sampleFunction(String a, String b){ return "「" + a + "」と「" + b + "」はclassAに定義されたsampleFunctionの引数"; } } class B implements sampleInterface{ public String sampleFunction(String a, String b){ return "「" + a + "」と「" + b + "」はclassBに定義されたsampleFunctionの引数"; } } A a = new A(); B b = new B(); System.out.println(a.sampleFunction("c","d")); System.out.println(b.sampleFunction("e","f")); } }出力結果(Java)
「c」と「d」はclassAに定義されたsampleFunctionの引数 「e」と「f」はclassBに定義されたsampleFunctionの引数コード例(Swift Protocol)
sample.swift// プロトコル protocol SampleProtocol{ func sampleFunction(_ a:String, _ b:String) -> String } // class A class A:SampleProtocol{ func sampleFunction(_ a:String, _ b:String) -> String{ return "「\(a)」と「\(b)」はclassAに定義されたsampleFunctionの引数" } } // class B class B:SampleProtocol{ func sampleFunction(_ a:String, _ b:String) -> String{ return "「\(a)」と「\(b)」はclassBに定義されたsampleFunctionの引数" } } var a = A() var b = B() print(a.sampleFunction("c","d")) print(b.sampleFunction("e","f"))出力結果(swift)
「c」と「d」はclassAに定義されたsampleFunctionの引数 「e」と「f」はclassBに定義されたsampleFunctionの引数
- 投稿日:2019-08-11T14:21:32+09:00
【iOS, Swift】Metal躓きポイント
※この記事は随時追加更新していきます。
AppleのGPU shader言語であるMetalを使っていて躓いたところについて色々と覚え書きです。
この記事は (現時点で) 以下の構成です。
- MTLTextureをdeep copyする方法
- MTLTextureをCVPixelBufferに変換する方法
- Metalで処理後の動画を保存する方法
MTLTextureをdeep copyする
MTLTextureオブジェクトはクラスインスタンスなので参照型です。CVPixelBufferなどの値型とは異なり複製には注意が必要です。
MTLTextureクラスには値渡しのメソッドはなく、したがってshallow copyを避けるためには自前で値渡しもしくは新規生成のコードを書くしかありません。
実際にはMTLTextureを別の画像オブジェクトに変換して、それを使って新たなMTLTextureを生成するのが良いと思います。
各種画像クラスを用いてMTLTextureクラスを初期化できますが、筆者はCGImageを使うのが最も楽であると思っています。// MTLDeviceは別途初期化された状態を想定。 // device: MTLDevice // MTLTextureからCIImage生成。画像の向きを調整。 let textureLoader = MTKTextureLoader(device: device) let textureLoaderOptions = [ MTKTextureLoader.Option.textureUsage: NSNumber(value: MTLTextureUsage.shaderRead.rawValue), MTKTextureLoader.Option.textureStorageMode: NSNumber(value: MTLStorageMode.`private`.rawValue) ] // CIImageに変換して画像の向きを整える。 let ciImage = CIImage(mtlTexture: texture!, options: nil)! .transformed(by: CGAffineTransform(scaleX: 1, y: -1) .translatedBy(x: 0, y: CGFloat(alphaTexture!.height))) // CIImage -> CGImage let ciContext = CIContext() guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else {return} // Create new MTLTexture let newTex = try? textureLoader.newTexture(cgImage: cgImage, options: textureLoaderOptions)MTLTexture -> CVPixelBuffer
CVPixelBufferLockBaseAddress(pixelBuffer, []) let pixelBufferBytes = CVPixelBufferGetBaseAddress(pixelBuffer)! let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) let region = MTLRegionMake2D(0, 0, texture.width, texture.height) // 以下を実行すれば、pixelBufferに画像データが入る。 texture.getBytes(pixelBufferBytes, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) CVPixelBufferUnlockBaseAddress(pixelBuffer, [])Metalで処理後に動画を保存する
AVAssetWriterを使います。
ここでは下記のようにVideoSaverクラスを定義します。import AVFoundation import Metal import MetalKit import Photos class VideoSaver { var isRecording = false var recordingStartTime = TimeInterval(0) private var url: URL? private var assetWriter: AVAssetWriter private var assetWriterVideoInput: AVAssetWriterInput private var assetWriterPixelBufferInput: AVAssetWriterInputPixelBufferAdaptor // AVAssetWriterを初期化 init?(outputURL url: URL, size: CGSize) { do { assetWriter = try AVAssetWriter(outputURL: url, fileType: AVFileType.mp4) } catch { return nil } self.url = url let outputSettings: [String: Any] = [ AVVideoCodecKey : AVVideoCodecType.h264, AVVideoWidthKey : size.width, AVVideoHeightKey : size.height ] assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: outputSettings) assetWriterVideoInput.expectsMediaDataInRealTime = true let sourcePixelBufferAttributes: [String: Any] = [kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA, kCVPixelBufferWidthKey as String : size.width, kCVPixelBufferHeightKey as String : size.height] assetWriterPixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterVideoInput, sourcePixelBufferAttributes: sourcePixelBufferAttributes) assetWriter.add(assetWriterVideoInput) } // UIButtonなどから呼ぶ録画開始メソッド func startRecording() { assetWriter.startWriting() assetWriter.startSession(atSourceTime: CMTime.zero) recordingStartTime = CACurrentMediaTime() isRecording = true } // UIButtonなどから呼ぶ録画終了メソッド func endRecording(_ completionHandler: @escaping () -> ()) { isRecording = false assetWriterVideoInput.markAsFinished() assetWriter.finishWriting(completionHandler: completionHandler) outputVideos() } // フレームをアプリローカルの保存先パスに書き込むメソッド。後述。 func writeFrame(forTexture texture: MTLTexture) { /* 後述 */ } // PHPhotoLibraryを用いて、アプリローカルの保存先からiOSのGalleryへファイルを移す。 private func outputVideos() { let url = self.url PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url!) }) { (isCompleted, error) in if isCompleted { do { try FileManager.default.removeItem(atPath: url!.path) print("ファイル移動成功 : \(url!.lastPathComponent)") } catch { print("ファイルコピー成功、ファイル移動失敗 : \(url!.lastPathComponent)") } } else { print("ファイルコピー失敗 : \(url!.lastPathComponent)") } } }作った.mp4動画はアプリのローカルディレクトリに一時保存するようにします。
録画開始・終了メソッド
録画開始・終了は以下のように呼べば良い。
func takeVideo() { if !isRec { guard let url = videoFileLocation() else {return} let size = CGSize(width: cTexture!.width, height: cTexture!.height) self.videoSaver = VideoSaver(outputURL: url, size: size) videoSaver!.startRecording() isRec = true } else { isRec = false videoSaver!.endRecording { print("Save Finished") } } }textureを取得してAVAssetWriterに渡す
textureをストリームで得るために以下の位置でcurrentDrawableからMTLTextureを取得、を取得するようにします。
/* 下記Metal描画パラメータの各種設定はすべて省略 let view = self.view as? MTKView view.device = MTLCreateSystemDefaultDevice() view.delegate = self let renderDestination = view let renderPassDescriptor = renderDestination.currentRenderPassDescriptor let currentDrawable = renderDestination.currentDrawable let device = view.device let commandQueue = device.makeCommandQueue() let commandBuffer = commandQueue.makeCommandBuffer() let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) */ renderEncoder.endEncoding() // .endEncoding() ~ .present() の間に、以下を挿入。 self.cTexture = currentDrawable.texture if self.isRec { commandBuffer.addCompletedHandler { commandBuffer in self.videoSaver!.writeFrame(forTexture: self.cTexture!) } } commandBuffer.present(currentDrawable) commandBuffer.commit()textureをアプリローカルに一時保存
最後に、以下がアプリローカルへのフレームデータ書き込みメソッドです。
func writeFrame(forTexture texture: MTLTexture) { if !isRecording {return} while !assetWriterVideoInput.isReadyForMoreMediaData {} // MTLTexure -> CVPixelBuffer -> AVAssetWriter's pixelBufferPool guard let pixelBufferPool = assetWriterPixelBufferInput.pixelBufferPool else {return} var maybePixelBuffer: CVPixelBuffer? = nil let status = CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &maybePixelBuffer) if status != kCVReturnSuccess {return} guard let pixelBuffer = maybePixelBuffer else { return } CVPixelBufferLockBaseAddress(pixelBuffer, []) let pixelBufferBytes = CVPixelBufferGetBaseAddress(pixelBuffer)! let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) let region = MTLRegionMake2D(0, 0, texture.width, texture.height) texture.getBytes(pixelBufferBytes, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) // AVAssetWriterへCVPixelBufferを追加。 let frameTime = CACurrentMediaTime() - recordingStartTime let presentationTime = CMTimeMakeWithSeconds(frameTime, preferredTimescale: 240) assetWriterPixelBufferInput.append(pixelBuffer, withPresentationTime: presentationTime) CVPixelBufferUnlockBaseAddress(pixelBuffer, []) }終わりに
ご参考になれば幸いです!
改善方法やご意見などあれば、どしどしコメント下さい!
- 投稿日:2019-08-11T12:19:10+09:00
flutterでfirebase_authを追加したらRunning Xcode build...から進まない現象の対処法
pubspec.yamlに
dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 rxdart: ^0.22.1+1 firebase_auth: ^0.14.0+と「firebase_auth」を追記すると、永遠に「Running Xcode build...」で終わらない。
対処法
Firebase flutter-setupを確認すると、ios/RunnerにGoogleService-Info.plistを入れろと書いてあるけど、直接フォルダに入れても同じ現象になるので注意。
xcode上からGoogleService-Info.plistを追加してあげないと認識できないみたいで、この手順を実施すれば正常にビルドできるようになる。
おわりに
わかるかこんなの!2時間ハマったわ。だからネイティブは(ry
- 投稿日:2019-08-11T06:48:20+09:00
HealthKitを使ってアプリがクラッシュするケース
HealthKitを使ってrequestAuthorization()の直後にアプリがクラッシュする場合、plistに追加した「使用する理由」が短すぎるのが原因かも。
確認した環境
iOS: 12.3
Xcode: 10.2.1遭遇した現象
HealthKitを使うための手続き
・HealthKitのCapabilityをONにする
・plistファイルにNSHealthShareUsageDescriptionキーを追加して理由を記載
・requestAuthorization()でHealthKitへのアクセス許可を求める
と、アプリケーションがクラッシュした。上記だけの動作をさせてもクラッシュする&ステップ実行しても当たりがつかず、途方にくれる。。。
対処方法
plistファイルに追加したNSHealthShareUsageDescriptionキーの値を、
"hoge"から"hoge hhoge hoge hoge hoge hoge hoge hoge hoge hoge hoge hoge "と
とりあえず長くしてみるとクラッシュしなくなる。。。とりあえず動かしてみようとして適当な文字列を入れてハマりました。
「文字列が短すぎるとクラッシュする」ということを聞いたのでやってみると解決。
エラー(原因)を表示してほしい!!!