20200720のiOSに関する記事は8件です。

【Swift】可変長引数の関数に配列を渡したい!ときのworkaround

結論

自前の関数なら同名の関数をoverloadできます。

 // 配列を引数に取る関数側にロジックを実装する
 func greet(_ people: [String]) {
     print("Hello, \(people.joined(separator: ", "))!")
 }

 // 可変長引数を持つ関数側では、内部で配列を引数に取る関数を呼び出す
 func greet(_ people: String...) {
     greet(people)
 }

言語仕様として、引数に渡すときに配列を展開することはできないので、フレームワーク内の可変長引数(例えば、printなど)の関数に配列を渡すのは現状は難しいようです。

参考

Passing an array to a function with variable number of args in Swift

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】\()を使用せず、文字列と一緒に変数をコンソール出力する方法

コンソールに文字列と一緒に変数を出力したい際、下記のように「 \ (変数)」を使用する方が多いと思います。

var foodList = ["カレー", "ラーメン", "カツ丼"]
print("食べ物: \(foodList)")
//出力結果 食べ物: ["カレー", "ラーメン", "カツ丼"]

実は下記のように、変数の前に「 , 」をつけるだけでもOKなんです。
「 \ ()」をタイプするよりも圧倒的に楽なのでオススメです。

print("食べ物: ", foodList)
//出力結果 食べ物: ["カレー", "ラーメン", "カツ丼"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

審査へ提出 審査へ提出するための項目が足りません エラー

App store connectのUIに変更が入っていますね。

いつもの感じで審査にすすめるために 審査へ提出 ボタンを押したところ

審査へ提出するための項目が足りません
提出には下記の項目が必要です:
広告ID(IDFA)の利用に関する正しい回答を選択していることを確認してください。AppにIDFAが含まれていて「いいえ」を選択した場合は、バイナリが恒久的に却下されます。この場合、新しいバイナリを提出する必要があります。

image.png

とエラーが出てしまいました。

ん?え?どこ?となりましたが、画面一番下にありました。

image.png

もともと、審査に提出 を押した次の画面にあった項目でした。バージョン別のページ一番したに移動したようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ディクショナリからタプルの配列を作成する

はじめに

  • Swift には、連想配列的なデータ構造としてディクショナリがあります
  • ディクショナリはデータの順序を保証しないので、そのままではテーブルビューやコレクションビューのデータソースとして利用しにくいです
  • その場合は、タプルの配列に変換すると上手く行きます

検証環境

  • Xcode 11.5
  • Swift 5.2

利用する機能

sorted(by:)

  • ディクショナリのメソッド
  • 引数に渡した関数 by がソート条件
    • 関数 by の引数
      • 2 つのタプル(それぞれディクショナリの n 番目の要素と n + 1 番目の要素に対応)
    • 関数 by の戻り値
      • Boolfalse なら並び替え)
  • 戻り値は [(key: ディクショナリの key の型, value: ディクショナリの value の型)]
    • つまり、ディクショナリとほぼ同じ構造を持った「タプルの配列」

コード例

  • 以下はディクショナリ [String: Int] からタプルの配列 [(key: String, value: Int)] に変換する例です
SortedDictionary.playground
import Foundation

var scores: [String: Int] = [
    "Steve Yamada": 34,
    "Jeff Takeshita": 87,
    "Mickey Yoshida": 100,
    "Charly Kinoshita": 53,
    "Anna Saito": 19,
    "Robert Suzuki": 97,
    "Erick Kawakami": 32,
    "John Miyabe": 64,
    "Gregory Goto": 76
]

print("""

    Ascending by key
    ========================
    """)
var sortedByNameAsc: [(key: String, value: Int)] = scores.sorted { $0.key < $1.key }
sortedByNameAsc.forEach { print("\($0.key): \($0.value)") }

print("""

    Descending by key
    ========================
    """)
var sortedByNameDesc: [(key: String, value: Int)] = scores.sorted { $0.key > $1.key }
sortedByNameDesc.forEach { print("\($0.key): \($0.value)") }

print("""

    Ascending by value
    ========================
    """)
var sortedByScoreAsc: [(key: String, value: Int)] = scores.sorted { $0.value < $1.value }
sortedByScoreAsc.forEach { print("\($0.value): \($0.key)") }

print("""

    Descending by value
    ========================
    """)
var sortedByScoreDesc: [(key: String, value: Int)] = scores.sorted { $0.value > $1.value }
sortedByScoreDesc.forEach { print("\($0.value): \($0.key)") }
出力結果
Ascending by key
========================
Anna Saito: 19
Charly Kinoshita: 53
Erick Kawakami: 32
Gregory Goto: 76
Jeff Takeshita: 87
John Miyabe: 64
Mickey Yoshida: 100
Robert Suzuki: 97
Steve Yamada: 34

Descending by key
========================
Steve Yamada: 34
Robert Suzuki: 97
Mickey Yoshida: 100
John Miyabe: 64
Jeff Takeshita: 87
Gregory Goto: 76
Erick Kawakami: 32
Charly Kinoshita: 53
Anna Saito: 19

Ascending by value
========================
19: Anna Saito
32: Erick Kawakami
34: Steve Yamada
53: Charly Kinoshita
64: John Miyabe
76: Gregory Goto
87: Jeff Takeshita
97: Robert Suzuki
100: Mickey Yoshida

Descending by value
========================
100: Mickey Yoshida
97: Robert Suzuki
87: Jeff Takeshita
76: Gregory Goto
64: John Miyabe
53: Charly Kinoshita
34: Steve Yamada
32: Erick Kawakami
19: Anna Saito

まとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

写経してみた(ARKit)

はじめに

前々から ARKit さわってみたいな〜と思いつつずっとさわってなかったんですが最近ずっと引きこもってて時間があるので ARKit をさわってみました。
新しいことを習得するには守破離の精神からいって「守」つまり真似することが大事かなと思います。プログラミングでいうと写経ですね。

ということで ARKit で写経してみました。

ar_sample

環境

  • Xcode 11.5
  • Deployment Target 13.0

とりあえずAR表示

今まで ARKit を全くさわってなかったのでほぼ何もわからない状態です。。。とりあえず ARKitのサンプルコード集「ARKit-Sampler」 を参考に 3D モデルを表示してみました。
手順は下記

  1. SceneKit Catalog を追加
    scenekit
  2. 3D モデルを追加(参考サイトのサンプルのship追加)
  3. Info.plist に Privacy - Camera Usage Description 追加
    info_plist
  4. IB上で ARSCNView 追加(ViewController に接続)
    ib
  5. ARKit.framework 追加(これがないとlink errorになった。。。)
    framework
  6. ViewController にコード記載
import UIKit
import ARKit

final class ViewController: UIViewController {

    @IBOutlet private weak var sceneView: ARSCNView!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        sceneView.scene = SCNScene(named: "art.scnassets/ship/ship.scn")!
        let config = ARWorldTrackingConfiguration()
        sceneView.session.run(config)
    }
}

これだけで AR が実現できます:clap:(もっと AVCapture とかでゴリゴリしないといけないと思ってました。。。)

写経

とりあえず 3D モデルが簡単に表示できることはわかった。次に何やろうと考えたときに 3D 空間に般若心経を写経したい!と思い至りました!!
写経するのに必要なのは下記

  1. 文字を表示する
  2. 文字にフェードアニメーションを付ける

とりあえずこの2つができれば写経できるはず!

文字を表示する

こちら(ARKitのAR文字をキレイにした話)を参考に文字表示してみました。とりあえず文字表示は SCNText を使うみたい。
まるっとコピペですがこれで表示できるみたいです。

//カメラの現在位置を取得する
guard let camera = sceneView.pointOfView else { return }

let textGeometry = SCNText(string: message, extrusionDepth: 0.8)
textGeometry.firstMaterial?.diffuse.contents = UIColor(named: "ArizarARFontColor")
textGeometry.font = UIFont(name: "HiraginoSans-W6", size: 100)
let textNode = SCNNode(geometry: textGeometry)
let position = SCNVector3(0,0.1,-0.1)
textNode.position = camera.convertPosition(position, to: nil)
//カメラの向きに合わせる
textNode.eulerAngles = camera.eulerAngles
//大きさ設定
textNode.scale = SCNVector3(0.0001,0.0001,0.001)

sceneView.scene.rootNode.addChildNode(textNode)

文字にフェードアニメーションを付ける

こちら(SceneKitのアニメーションサンプル集)を参考にアニメーションを付けてみました。アニメーションには SCNAction を使うみたい。fadeIn っていうそれっぽいのがあったので使いました。

textNode.opacity = 0.0 // 0にしておく
let action = SCNAction.fadeIn(duration: 0.2)
textNode.runAction(action)

ちょっとひっかかったのが fadeInopacity を 1.0 に変更してくれるやつなのでもともとの opacity を下げとかないと何も起きません。

全体の実装

import UIKit
import ARKit
import SceneKit

final class ViewController: UIViewController {

    @IBOutlet private weak var sceneView: ARSCNView!
    private var texts = [
        "仏説摩訶般若波羅蜜多心経",
        "観自在菩薩", "行深般若波羅蜜多時", "照見五蘊皆空", "度一切苦厄",
        "舎利子", "色不異空", "空不異色", "色即是空", "空即是色",
        "受想行識亦復如是", "舎利子", "是諸法空相", "不生不滅", "不垢不浄", "不増不減",
        "是故空中", "無色", "無受想行識", "無眼耳鼻舌身意", "無色声香味触法", "無眼界", "乃至無意識界",
        "無無明", "亦無無明尽", "乃至無老死", "亦無老死尽",
        "無苦集滅道", "無智亦無得", "以無所得故", "菩提薩埵", "依般若波羅蜜多故", "心無罣礙", "無罣礙故", "無有恐怖", "遠離一切顛倒夢想",
        "究竟涅槃",
        "三世諸仏", "依般若波羅蜜多故", "得阿耨多羅三藐三菩提", "故知般若波羅蜜多",
        "是大神呪", "是大明呪", "是無上呪", "是無等等呪", "能除一切苦", "真実不虚", "故説般若波羅蜜多呪", "即説呪日",
        "羯諦", "羯諦", "波羅羯諦", "波羅僧羯諦", "菩提薩婆訶", "般若心経"
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        sceneView.scene = SCNScene()
        let config = ARWorldTrackingConfiguration()
        sceneView.session.run(config)
    }

    @IBAction private func start(_ sender: Any) {
        guard let camera = sceneView.pointOfView else { return }
        // カメラ中央手前に表示
        let startPosition = camera.convertPosition(SCNVector3(0, 0, -0.5), to: nil)
        // 文字サイズたぶん3cm
        let textSize: Float = 0.03
        var index = 0
        var x = startPosition.x
        for text in texts {
            var y = startPosition.y
            for t in text {
                let textGeometry = makeSCNText(String(t))
                let textNode = SCNNode(geometry: textGeometry)
                textNode.position = SCNVector3(x, y, startPosition.z)
                let scale = 1 / (textGeometry.boundingSphere.radius * 2) * textSize
                textNode.scale = SCNVector3(x:scale, y:scale, z:scale)
                textNode.opacity = 0.0
                y -= (textSize + textSize/3)
                let action = makeFadeAnimation(index: index)
                textNode.runAction(action)
                sceneView.scene.rootNode.addChildNode(textNode)
                index += 1
            }
            x -= (textSize + textSize/3)
        }
    }

    private func makeSCNText(_ text: String) -> SCNText {
        let textGeometry = SCNText(string: text, extrusionDepth: 0.2)
        textGeometry.firstMaterial?.diffuse.contents = UIColor.white
        textGeometry.font = .systemFont(ofSize: 1)
        return textGeometry
    }

    private func makeFadeAnimation(index: Int) -> SCNAction {
        let duration = 0.2
        let waitAction  = SCNAction.wait(duration: duration * Double(index))
        let fadeAction  = SCNAction.fadeIn(duration: duration)
        return SCNAction.sequence([waitAction, fadeAction])
    }
}

課題

とりあえず動くものはできましたがまだどの Node をどれの Child にすべきかとかはよくわかってないです。。。
文字サイズを決めてる let scale = 1 / (textGeometry.boundingSphere.radius * 2) * textSize もいまいちなんでこれで計算できるのかわかってません:weary:

「観」とか一部の文字がなぜか表示できませんでした。

おいおい勉強していこうと思います:muscle:

さいごに

これをやるために般若心経についてちょっと調べました(2冊本読んだ:sunglasses:)がすべてのものは存在するようで存在しないものなんだとか。
みなさんも開発で仕様がコロコロ変わるのを経験したことがあると思いますがそういうときにイライラしてはいけません。仕様もまた存在するようで存在しないものなのです。そう、仕様もまた「空」なのです:innocent:

これ:point_right:「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。みたときからやりたかった:blush:

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マスク未着用検知アプリをApple公式フレームワークで実装する

iPadやiPhoneのカメラ・フレームを分析して、マスクを着用していない人を検知できるアプリをかんたんにつくることができます。
お店の入り口などに置いてください。

人間の顔を見つける

カメラ・フレームの中にマスクを着用していない人がいるかどうか特定するためには、そこに鼻や口の隠れていない顔がうつっているかどうかをたしかめればいいです。

Vision

そのために、AppleのVisionフレームワークを使うことができます。
Visionは人間の顔を検知する機能を含んでいます。
そして、マスクを着用していると、Visionはそれを顔だと検知できません。
ゆえに、カメラ画像に顔があるとVisionが言うならば、その顔はマスクをつけていません。

顔を検知するVisionの機能は、VNDetectFaceRectanglesRequestです。
このリクエストにカメラ・フレームを渡すと、その画像の中に人間の顔がうつっているかと、その位置を返してくれます。

以下のように設定します。

Vision.swift
@discardableResult
      func setupVision() -> NSError? {
            // Setup Vision parts
            let error: NSError! = nil
            let detectFaceRequest:VNDetectFaceRectanglesRequest = {
                    let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.processVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()
                self.requests = [faceCropRequest]

            return error
        }

Visionにカメラ画像を渡して解析してもらう

次に、カメラフレームからの画像をこのリクエストに1フレームずつ渡していきます。
iPadやiPhoneのカメラを使うには、AVFoundationを使えます。

Vision.swift
import UIKit
import Foundation
import Vision
import AVFoundation

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
         if isRequest {
             guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                 return
             }
             currentBuffer = pixelBuffer

             let exifOrientation = exifOrientationFromDeviceOrientation()
             let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: exifOrientation, options: [:])
             do {
                 try imageRequestHandler.perform(self.requests)
             } catch {
                 print(error)
             }
             isRequest = false
         }
     }

     func setupAVCapture() {
            var deviceInput: AVCaptureDeviceInput!

            // Select a video device, make an input
            let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front).devices.first
            do {
                deviceInput = try AVCaptureDeviceInput(device: videoDevice!)
            } catch {
                print("Could not create video device input: \(error)")
                return
            }

            session.beginConfiguration()
            session.sessionPreset = .vga640x480 // Model image size is smaller.

            // Add a video input
            guard session.canAddInput(deviceInput) else {
                print("Could not add video device input to the session")
                session.commitConfiguration()
                return
            }
            session.addInput(deviceInput)
            if session.canAddOutput(videoDataOutput) {
                session.addOutput(videoDataOutput)
                // Add a video data output
                videoDataOutput.alwaysDiscardsLateVideoFrames = true
                videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
                videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
            } else {
                print("Could not add video data output to the session")
                session.commitConfiguration()
                return
            }
            let captureConnection = videoDataOutput.connection(with: .video)
            captureConnection?.videoOrientation = .portrait
            // Always process the frames
            captureConnection?.isEnabled = true
            do {
                try  videoDevice!.lockForConfiguration()
                let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
                bufferSize.width = CGFloat(dimensions.height)
                bufferSize.height = CGFloat(dimensions.width)
                videoDevice!.unlockForConfiguration()
            } catch {
                print(error)
            }
            session.commitConfiguration()

            setupVision()

            // start the capture
            startCaptureSession()
        }

        func startCaptureSession() {
            session.startRunning()
        }

        public func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation {
            let curDeviceOrientation = UIDevice.current.orientation
            let exifOrientation: CGImagePropertyOrientation

            switch curDeviceOrientation {
            case UIDeviceOrientation.portraitUpsideDown:  // Device oriented vertically, home button on the top
                exifOrientation = .left
            case UIDeviceOrientation.landscapeLeft:       // Device oriented horizontally, home button on the right
                exifOrientation = .upMirrored
            case UIDeviceOrientation.landscapeRight:      // Device oriented horizontally, home button on the left
                exifOrientation = .down
            case UIDeviceOrientation.portrait:            // Device oriented vertically, home button on the bottom
                exifOrientation = .up
            default:
                exifOrientation = .up
            }
            return exifOrientation
        }

        @discardableResult
        func setupVision() -> NSError? {
            // Setup Vision parts
            let error: NSError! = nil

                let faceCropRequest:VNDetectFaceRectanglesRequest = {
                    let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.drawVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()

                self.requests = [faceCropRequest]

            return error
        }

        func processVisionRequestResults(_ results: [Any]) {
            if currentBuffer != nil {
                currentBuffer = nil
                guard let observation = results.first as? VNFaceObservation else {
                    masking()
                    return
                }
                noMasking()
            }
        }
}

AVCaptureVideoDataOutputSampleBufferDelegateという毎カメラ・フレームを処理する関数が、毎フレームの画像をVisionの顔検出リクエストに渡しているというコードです。

以下の部分で、カメラ画像で最初に書いたリクエスト処理を実行しています。

Vision.swift
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
         if isRequest {
             guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                 return
             }
             currentBuffer = pixelBuffer

             let exifOrientation = exifOrientationFromDeviceOrientation()
             let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: exifOrientation, options: [:])
             do {
                 try imageRequestHandler.perform(self.requests)
             } catch {
                 print(error)
             }
             isRequest = false
         }
     }

結果(マスクしてない人がうつっているか)を処理する

リクエストが完了した時に呼ばれる完了ハンドラーというのがあります。
そのなかで結果を処理します。

Vision.swift
let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.processVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()

request.resultsというのがリクエスト結果です。もし画像に顔が含まれていれば、リクエスト結果は、画像に顔が含まれていたというobservation(観察)を持っています。VNFaceObservationです。
もし結果がこれを持っていれば、マスク未着用処理をします。顔がうつっているからです。

Vision.swift
        func processVisionRequestResults(_ results: [Any]) {
            if currentBuffer != nil {
                currentBuffer = nil
                guard let observation = results.first as? VNFaceObservation else {
                    masking()
                    return
                }
                noMasking()
            }
        }

未着用なら
スクリーンショット 2020-05-13 2.06.02.png
着用なら
スクリーンショット 2020-05-13 2.06.50.png
を表示したりとか。

AVFoundationのAVSpeechSynthesizerを使うと、音声で「マスクの着用をお願いします」と呼びかけてくれます。

ViewController.swift
    var talker = AVSpeechSynthesizer()
    var talking = false

    func noMasking() {

    if !talking {
            let utterance = AVSpeechUtterance(string: NSLocalizedString("Please wear a mask.", comment: "") )
            utterance.voice = AVSpeechSynthesisVoice(language: NSLocalizedString("en-US", comment: ""))
            talker.speak(utterance)
            talking = true
    }

ちなみに、VNDetectFaceObservationは顔を囲む四角形の座標もくれます。
カメラ画像に「マスク未着用」の四角警告をオーバーラップさせると、アプリがより監視してるテイストになります。

実際のアプリはこちら

GitHubにアプリ置いときます。
https://github.com/john-rocky/MaskPlease

ぼくのTwitterをフォローしてください。お願いします。
https://twitter.com/JackdeS11

あと、Looks Good For Me(わるくないね)、押してください。

チャオ?!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マスク未着用検知アプリをVision(Apple公式フレームワーク)で実装する

iPadやiPhoneのカメラ・フレームを分析して、マスクを着用していない人を検知できるアプリをかんたんにつくることができます。
お店の入り口などに置いてください。
スクリーンショット 2020-07-21 0.56.51.png

人間の顔を見つける

カメラ・フレームの中にマスクを着用していない人がいるかどうか特定するためには、そこに鼻や口の隠れていない顔がうつっているかどうかをたしかめればいいです。

Vision

そのために、AppleのVisionフレームワークを使うことができます。
Visionは人間の顔を検知する機能を含んでいます。
そして、マスクを着用していると、Visionはそれを顔だと検知できません。
ゆえに、カメラ画像に顔があるとVisionが言うならば、その顔はマスクをつけていません。
スクリーンショット 2020-07-21 1.01.50.png
スクリーンショット 2020-07-21 1.04.22.png

顔を検知するVisionの機能は、VNDetectFaceRectanglesRequestです。
このリクエストにカメラ・フレームを渡すと、その画像の中に人間の顔がうつっているかと、その位置を返してくれます。

以下のように設定します。

Vision.swift
@discardableResult
      func setupVision() -> NSError? {
            // Setup Vision parts
            let error: NSError! = nil
            let detectFaceRequest:VNDetectFaceRectanglesRequest = {
                    let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.processVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()
                self.requests = [faceCropRequest]

            return error
        }

Visionにカメラ画像を渡して解析してもらう

次に、カメラフレームからの画像をこのリクエストに1フレームずつ渡していきます。
iPadやiPhoneのカメラを使うには、AVFoundationを使えます。

Vision.swift
import UIKit
import Foundation
import Vision
import AVFoundation

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
         if isRequest {
             guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                 return
             }
             currentBuffer = pixelBuffer

             let exifOrientation = exifOrientationFromDeviceOrientation()
             let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: exifOrientation, options: [:])
             do {
                 try imageRequestHandler.perform(self.requests)
             } catch {
                 print(error)
             }
             isRequest = false
         }
     }

     func setupAVCapture() {
            var deviceInput: AVCaptureDeviceInput!

            // Select a video device, make an input
            let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .front).devices.first
            do {
                deviceInput = try AVCaptureDeviceInput(device: videoDevice!)
            } catch {
                print("Could not create video device input: \(error)")
                return
            }

            session.beginConfiguration()
            session.sessionPreset = .vga640x480 // Model image size is smaller.

            // Add a video input
            guard session.canAddInput(deviceInput) else {
                print("Could not add video device input to the session")
                session.commitConfiguration()
                return
            }
            session.addInput(deviceInput)
            if session.canAddOutput(videoDataOutput) {
                session.addOutput(videoDataOutput)
                // Add a video data output
                videoDataOutput.alwaysDiscardsLateVideoFrames = true
                videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
                videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
            } else {
                print("Could not add video data output to the session")
                session.commitConfiguration()
                return
            }
            let captureConnection = videoDataOutput.connection(with: .video)
            captureConnection?.videoOrientation = .portrait
            // Always process the frames
            captureConnection?.isEnabled = true
            do {
                try  videoDevice!.lockForConfiguration()
                let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
                bufferSize.width = CGFloat(dimensions.height)
                bufferSize.height = CGFloat(dimensions.width)
                videoDevice!.unlockForConfiguration()
            } catch {
                print(error)
            }
            session.commitConfiguration()

            setupVision()

            // start the capture
            startCaptureSession()
        }

        func startCaptureSession() {
            session.startRunning()
        }

        public func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation {
            let curDeviceOrientation = UIDevice.current.orientation
            let exifOrientation: CGImagePropertyOrientation

            switch curDeviceOrientation {
            case UIDeviceOrientation.portraitUpsideDown:  // Device oriented vertically, home button on the top
                exifOrientation = .left
            case UIDeviceOrientation.landscapeLeft:       // Device oriented horizontally, home button on the right
                exifOrientation = .upMirrored
            case UIDeviceOrientation.landscapeRight:      // Device oriented horizontally, home button on the left
                exifOrientation = .down
            case UIDeviceOrientation.portrait:            // Device oriented vertically, home button on the bottom
                exifOrientation = .up
            default:
                exifOrientation = .up
            }
            return exifOrientation
        }

        @discardableResult
        func setupVision() -> NSError? {
            // Setup Vision parts
            let error: NSError! = nil

                let faceCropRequest:VNDetectFaceRectanglesRequest = {
                    let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.drawVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()

                self.requests = [faceCropRequest]

            return error
        }

        func processVisionRequestResults(_ results: [Any]) {
            if currentBuffer != nil {
                currentBuffer = nil
                guard let observation = results.first as? VNFaceObservation else {
                    masking()
                    return
                }
                noMasking()
            }
        }
}

AVCaptureVideoDataOutputSampleBufferDelegateという毎カメラ・フレームを処理する関数が、毎フレームの画像をVisionの顔検出リクエストに渡しているというコードです。

以下の部分で、カメラ画像で最初に書いたリクエスト処理を実行しています。

Vision.swift
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
         if isRequest {
             guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                 return
             }
             currentBuffer = pixelBuffer

             let exifOrientation = exifOrientationFromDeviceOrientation()
             let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: exifOrientation, options: [:])
             do {
                 try imageRequestHandler.perform(self.requests)
             } catch {
                 print(error)
             }
             isRequest = false
         }
     }

結果(マスクしてない人がうつっているか)を処理する

リクエストが完了した時に呼ばれる完了ハンドラーというのがあります。
そのなかで結果を処理します。

Vision.swift
let request = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
                        DispatchQueue.main.async(execute: {
                            // perform all the UI updates on the main queue
                            if let results = request.results {
                                self.processVisionRequestResults(results)
                            }
                        })
                    })
                    request.revision = VNDetectFaceRectanglesRequestRevision2
                    return request
                }()

request.resultsというのがリクエスト結果です。もし画像に顔が含まれていれば、リクエスト結果は、画像に顔が含まれていたというobservation(観察)を持っています。VNFaceObservationです。
もし結果がこれを持っていれば、マスク未着用処理をします。顔がうつっているからです。

Vision.swift
        func processVisionRequestResults(_ results: [Any]) {
            if currentBuffer != nil {
                currentBuffer = nil
                guard let observation = results.first as? VNFaceObservation else {
                    masking()
                    return
                }
                noMasking()
            }
        }

未着用なら
スクリーンショット 2020-05-13 2.06.02.png
着用なら
スクリーンショット 2020-05-13 2.06.50.png
を表示したりとか。

AVFoundationのAVSpeechSynthesizerを使うと、音声で「マスクの着用をお願いします」と呼びかけてくれます。

ViewController.swift
    var talker = AVSpeechSynthesizer()
    var talking = false

    func noMasking() {

    if !talking {
            let utterance = AVSpeechUtterance(string: NSLocalizedString("Please wear a mask.", comment: "") )
            utterance.voice = AVSpeechSynthesisVoice(language: NSLocalizedString("en-US", comment: ""))
            talker.speak(utterance)
            talking = true
    }

ちなみに、VNDetectFaceObservationは顔を囲む四角形の座標もくれます。
カメラ画像に「マスク未着用」の四角警告をオーバーラップさせると、アプリがより監視してるテイストになります。

実際のアプリはこちら

GitHubにアプリ置いときます。
https://github.com/john-rocky/MaskPlease

ぼくのTwitterをフォローしてください。お願いします。
https://twitter.com/JackdeS11

お仕事のご依頼をこのメールにお願いします。
rockyshikoku@gmail.com

あと、Looks Good For Me(わるくないね)、押してください。
        ここです ↓
チャオ?!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大学生が1週間でFlutterアプリを学んでリリースした過程(3日目)

こんばんはシオンです。

一週間でアプリをリリースすると決めてから3日目。
昨日今後の予定を立てて、あとは前に進むだけです。残りの工程としては、デザイン(今日)、プログラミング(4〜6日目)、リリース(7日目)。

今日は予定通りAdobeXDを使ってデザインを完成させます。さっそくやっていきましょう。

目次

■アプリのデザインを完成させる
■デザインを作る
■まとめ

■アプリのデザインを完成させる

まずは、AdobeXDを開きます。
デザインを作っていく上で、軸にするデバイス(iphone〜とかのやつ)ですが今回はiphone8で作っていきます。理由は今使われているデバイスの中で一番小さいからです。(昔のiphoneSEとかは考えません!)

ちなみに立ち上げた瞬間はこんな感じ。

1.png

さて、デザインを作っていきますがデザインを作るために昨日考えたことを思い出します。

<アプリのデザイン>
 ┣ デザインを作るソフト ━ AdobeXD
 ┣ デザインソフトを使う知識 ━ すでに持っているからOK
 ┗ ソフトでデザインを作るために必要な要素
       ┣ アプリのコンセプト ┳ 何をする? ━ 愚痴を書き込んで消化する。 ━ 消化して次に進むためのアプリ ━ だから灰色、黒などはNGでも赤、黄色もテンションが違う。
       ┃              ┣ 誰が使う? ━ 愚痴ある人、OL、20代の女性向け。 ━ 女性らしい色合いが良さそう。ピンク、紫
       ┃              ┣ いつどこで使う? ━ 愚痴が溜まった時、他にいう人がいない時。 ━ すぐに書き込みたいからトップ画面に愚痴書き込み画面にしたい。
       ┃              ┗ なぜ使う? ━ 吐き出して乗り越えるため。周りに言いたくないことを言う。言ってはいけない悩みを言う。 ━ 秘密、乗り越える勇気づけ、優しい、落ち着く ━ 紫色、緑色を使って秘密かつ落ち着く配色が良さそう。
       ┣ アプリの機能 ┳ 愚痴を書き込む ━ 書き込むフォーム、題名とかあったほうがいいか? あと客観的に自分の状況を見るためにその愚痴がどのくらい嫌かの愚痴レベルをつけると良さそう ━ 題名、愚痴の内容2つを書き込むフォーム。愚痴レベル評価ボタン。
       ┃           ┣ 消化する ━ 愚痴を消す。消化、乗り越えるだから、書き込み完了ボタン → 「愚痴を乗り越えて次に進みますか?」 → 「では今から何をしますか?」 みたいな感じで次の行動につなげる表示を出す。
       ┃           ┗ 消化した数を表示する。 ━ 乗り越えた愚痴の数を表示する。これまでの積み上げ、一つの勇気になるか? ━ 画面の右上に表示。余裕があれば別画面に表示してメッセージとかをつける。
       ┗ アプリの制作期間 ━ 一週間。 ━ Flutterの勉強をしながらになるから1~3画面に収める。

なるほど、なるほど即席にしてはなかなかいいですね。昨日の自分。

デザインを完成させるために必要なことを考えます。
デザインを作るためには
・アプリのコンセプト
・使う色
・画面数
・画面ごとの機能
この辺があれば作っていけそうです。一つ一つ決めていきます。

<アプリのコンセプト>
まず、アプリのコンセプトとしては昨日を参考にしましょう。
昨日考えたことを一言でまとめると
「ユーザーが愚痴を吐き出して前に進むことを後押しをするアプリ」
という感じでしょうか。ぱっと見良さそうですね。

このコンセプトから使う色を決めていきます。

<配色を決める>
このコンセプトの中でフォーカスする点としては、愚痴を吐き出す。前に進む後押し。この二つでしょうか。
愚痴を吐き出す = 秘密、落ち着き = 紫、緑、茶色
前に進む後押し = 勇気付ける、背中を押す = 黄色
こんな感じでしょうか。もう時間もないのでこれでいきましょう。
ちなみに色のイメージとかはぐぐれば出てきます。こちらを参考にしました。

<画面数と画面毎の機能>
これを決めるためにはまず、実装する機能を決定しましょう。
実装する機能としては
①愚痴を記入する機能
②愚痴を消す機能
③背中を後押しする機能

①と②はフォームを置いてボタンをつければ良さそうです。
③を考えます。
背中を押すためには昨日の考えから行くと
・次に何をするかを決める機能
・乗り越えた愚痴の数を表示する機能

この二つをつければ良さそうです。つまり今回作るアプリは

①愚痴を記入する

②愚痴を消す

③次に何をするかを決める

④乗り越えた数を表示する

というアプリになりそうです。
ここから画面数と画面毎の機能を決定していきます。

①と②の昨日は一緒の画面で行けそうです。
③の機能で1画面。
④は次に何をするかを決める画面で+1の表示。合計数はスタート画面を設けてそこに表示しましょう。

ということで、画面数と画面毎の機能としては
スタート画面: 乗り越えた数と愚痴を記入するページへのボタンを設置。
愚痴解消画面: 愚痴のタイトル、内容のフォームと消すボタンを設置。
乗り越え画面: 次の行動を記入するフォームと、ボタンを設置。できればメッセージとかあれば尚良し(時間次第)。

以上の3画面を作れば良さそうです。

■デザインを作る

では、さっそくこの3画面のデザインを決めていきます。

2.png

あ、そういえば今XDのファイルを保存するところで名前を決めなければいけないことに気づきました。何がいいでしょうか?とりあえず、解消くんにしておこうと思います(あとで絶対に変えます。)

では、スタート画面から作っていきます。
タイトルとボタンと乗り越える数を配置していきます。そして色を調節していきます。うーんこのパーツだけだと結構物寂しい感じになってしまいました。(配置と配色の問題か?)

3.png

これはだめですね。。。
これだと3つパーツあって何をどこをみたらいいのかわかりませんし、ダサいし。コンセプト的に乗り越えた数よりも愚痴を吐き出すがメインだからそっちを目立たせて、乗り越えた数はサブにします。

4.png

なんかこねくり回している間によく分からなくなってきました。(これは良くなっているのだろうか)
ちなみにこのしたの花はサザンカといい、花言葉は「困難に打ち勝つ」だそうです。コンセプトにあってるなーと思いましたが、なんか花に頼っておしゃれ感出そうとしてるのが見え透いているのがダサいですね。てか、解消くんって何・・・

今日デザインを完成させる予定だったのですが、だめです。全然出てきません。そもそも配色の設定が間違っているのかもしれません。

ちょっと日程的にキツキツですが明日デザインを決めてプログラミングに入ろうと思います。
これは雲行きが怪しくなってきましたね。。。

■まとめ

最初から完成する気が全くしていなかったのですが、今日で1%から0.01%に完成する確率が下がった気がします。。

どうでもいいんですけど、なぜFlutterを学ぶために始めたのに、3日目にしてコードが書けていないのだろうか。
いやでもリンカーンさんが準備は大事って言ってたしな。。
明日の自分に期待しましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む