20210221のSwiftに関する記事は7件です。

【Swift】Compact状態のDatePickerに戻る処理を行う

新しいDatePicker

iOS14からどうやらDatePickerの挙動が変わったらしく、今までのドラムロール式の日付・日時選択からカレンダー表示の様な選択方法が選べるようになりました(従来のDatePickerも引き続き使用出来る)。
しかしこのポップアップされるDatePickerは日付を選択時に一つ前に戻る機能が備わって居らず自分で書いて選択時に戻る処理を入れてあげる必要がある。
CompactDatePickerが表示されたiPhoneSimulator

コード

ViewController
@IBAction func CompactDatePicker(_ sender: UIDatePicker) {
    // 日付選択時に選択前の画面に戻る
    self.dismiss(animated: true, completion: nil)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftで曜日を全て表示

課題

Swiftで曜日を表示する + 表示形式はLocaleに従うようにするコードを書きたかった。

結果

protocol WeekDaySymbolsDrawable {
    var locale: Locale { get }
    var weekDaySymbols: [String] {get}
}


struct WeekDayDrawer: WeekDaySymbolsDrawable {
    var locale: Locale

    var weekDaySymbols:[String] {
        var calendar = Calendar.current
        calendar.locale = locale
        return calendar.shortWeekdaySymbols
    }
}

DateFormatterなどでやろうとしたが普通にcalendar.shortWeekdaySymbolsがあった!

localeはアプリでの利用なら勝手に設定されそう?

利用例: 日本語

WeekDayDrawer(locale: Locale(identifier: "ja_JP")).weekDaySymbols
    .forEach{print($0)}
結果







利用例: 英語

WeekDayDrawer(locale: Locale(identifier: "en_US")).weekDaySymbols
    .forEach{print($0)}
結果
Sun
Mon
Tue
Wed
Thu
Fri
Sat

利用例: フランス語

WeekDayDrawer(locale: Locale(identifier: "fr_FR")).weekDaySymbols
    .forEach{print($0)}
結果
dim.
lun.
mar.
mer.
jeu.
ven.
sam.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Multipeer ConnectivityでのP2P通信(Swift)

Xcode-12 Swift-5.3 iOS-14

はじめに

iOS の端末間で通信とかしたいなと思い Multipeer Connectivity について調べました。

ソース :point_right: GitHub

Multipeer Connectivityとは

Multipeer Connectivity は Wi-Fi と Bluetooth を使って iOS 端末間の近距離通信(ピア・ツー・ピア)ができるライブラリです(最大8個まで接続できる)。
Wi-Fi が OFF でも Bluetooth で接続できるのかと思いましたが、OFF だとアドバタイザーが見えないらしく ON にする必要があるようです(同一 Wi-Fi でなくても OK)。

接続の概要

接続の流れは下記のようになっています。

  1. MCPeerID を生成する。
    端末を識別する一意のもの
  2. MCSession を生成する。
    接続を管理するやつ
  3. MCNearbyServiceAdvertiser もしくは MCAdvertiserAssistant でアドバタイズする。
    他の端末から検知できるようにする
  4. MCNearbyServiceBrowser もしくは MCBrowserViewController で接続できる端末を探す。
  5. 発見した端末に対して招待を送る。
  6. 招待を受けた端末側で招待を許可すると接続が確立する。
  7. MCSession でデータの送受信を行う。
    データ受信は MCSessionDelegate を使う

各クラスの使い方

MCPeerID

MCPeerID

識別用の一意のもの。下記イニシャライザで displyaName(63 B 以下で UTF-8)を設定して生成する。
同名のものを設定しても別々の ID が生成される。空文字など異常値が設定されるとクラッシュする。

init(displayName: String)

アドバタイズや探索のたびに生成するのではなく同じものを利用するようにした方がよい(仮に advertiser と borowser に別の ID を設定すると自身が検知されてしまう)。

MCSession

MCSession

下記どちらかで生成する。

// identity: 証明書関連で使う(ちょっとわからない。。。)
// encryptionPreference: 通信を暗号化するかどうか
init(peer myPeerID: MCPeerID, securityIdentity identity: [Any]?, encryptionPreference: MCEncryptionPreference)

// encryptionPreferenceはrequiredになる
init(peer myPeerID: MCPeerID)

encryptionPreference は下記がある。

  • none: 暗号化しない
  • required: 暗号化する
  • optional: すべての端末が requiredoptional であれば暗号化する。
    none があれば暗号化しない

データ送信は下記を使う。

// Data型の送信
// mode: reliableなら送信順の保証、unreliableなら送信順は保証されないが即時送信
func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws

// ファイル送信
// completionHandler: errorは成功時はnil
// 戻り値はキャンセルとかに使う
func sendResource(at resourceURL: URL, withName resourceName: String, toPeer peerID: MCPeerID, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) -> Progress?

// ストリーム?(ちょっとわからない。。。)
func startStream(withName streamName: String, toPeer peerID: MCPeerID) throws -> OutputStream

接続関連。

// 接続済み端末一覧
var connectedPeers: [MCPeerID]

// 接続を切断する
func disconnect()

// 手動接続の場合に利用する?(ちょっとわからない。。。)
func nearbyConnectionData(forPeer peerID: MCPeerID, withCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void)
func connectPeer(_ peerID: MCPeerID, withNearbyConnectionData data: Data)
func cancelConnectPeer(_ peerID: MCPeerID)

デリゲート(MCSessionDelegate)。

データ受信時などに UI を更新する場合はメインスレッドで処理するように注意。

// 接続状態変更通知(required)
// state: connected, notConnected, connectingがある
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState)

// データ受信(required)
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)

// ファイル受信開始(required)
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress)

// ファイル受信終了(required)
// localURL:一時ファイルのURLreturnするまでに保存とかしないと消える
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?)

// バイトストリーム接続(required)
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID)

// 証明書関連のやつ(optional)
func session(MCSession, didReceiveCertificate: [Any]?, fromPeer: MCPeerID, certificateHandler: (Bool) -> Void)

MCNearbyServiceAdvertiser

MCNearbyServiceAdvertiser

下記で生成する。serviceType は 1〜15 文字で、ASCII 小文字、数字、ハイフンが使えます(--のように連続したハイフンは不可)。空文字など異常値が設定されるとクラッシュする。

// discoveryInfo: アドバタイズ時に付加する情報
init(peer: MCPeerID, discoveryInfo: [String : String]?, serviceType: String)

アドバタイズ。

/// アドバタイズ開始
func startAdvertisingPeer()
/// アドバタイズ停止
func stopAdvertisingPeer()

デリゲート(MCNearbyServiceAdvertiserDelegate)。

// 招待を受けたときに呼ばれる(required)
// context: 招待相手の情報。ブラウザのinvitePeerで設定した値
// invitationHandler: 招待を許可するかどうか設定
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void)

// アドバタイズ失敗時に呼ばれる(optional)
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error)

MCAdvertiserAssistant

原因はよくわかりませんが許可アラートが表示されず使えないようですとりあえず MCNearbyServiceAdvertiser を使えば接続できました。

MCAdvertiserAssistant

下記で生成する。

// discoveryInfo: アドバタイズ時に付加する情報
init(serviceType: String, discoveryInfo: [String : String]?, session: MCSession)

アドバタイズ。

// アドバタイズ開始
func start()
// アドバタイズ停止
func stop()

デリゲート(MCAdvertiserAssistantDelegate)。

// optional
func advertiserAssistantDidDismissInvitation(_ advertiserAssistant: MCAdvertiserAssistant)

// optional
func advertiserAssistantWillPresentInvitation(_ advertiserAssistant: MCAdvertiserAssistant)

MCNearbyServiceBrowser

MCNearbyServiceBrowser

細かい設定がいらない場合は MCBrowserViewController を使う方が楽。

下記で生成する。serviceType に設定できる値はアドバタイズで設定できるものと同様。

init(peer: MCPeerID, serviceType: String)

検索。

// 検索開始
func startBrowsingForPeers()
// 検索停止
func stopBrowsingForPeers()

招待。

// context: 招待相手に渡す情報。これで相手側で招待を受けるか判断したりする
// timeout: 秒単位で0以下を設定するとデフォルト値の30秒になる
func invitePeer(_ peerID: MCPeerID, to session: MCSession, withContext context: Data?, timeout: TimeInterval)

デリゲート(MCNearbyServiceBrowserDelegate)。

// 検索開始失敗に呼ばれる(optional)
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error)

// 端末を発見したときに呼ばれる(required)
// info: アドバタイズで設定されているのdiscoveryInfo
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?)

// 発見した端末ロスト時に呼ばれる(required)
// 対象の招待不可
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID)

MCBrowserViewController

MCBrowserViewController

簡易に検索と画面表示をおこなってくれる ViewController。表示するときは push ではなく present を使う(Cancel, Done のナビゲーションバーがあるので push するとバーの下にバーが表示される)。iPad の場合はフルスクリーンではなく pageSheet?表示になるようです。

init(serviceType: String, session: MCSession)
init(browser: MCNearbyServiceBrowser, session: MCSession)

// 接続端末の最大値(デフォルト値と最大値は8)
var maximumNumberOfPeers: Int

// 接続端末の最小値(デフォルト値と最小値は2)
var minimumNumberOfPeers: Int

デリゲート(MCBrowserViewControllerDelegate)。

// 発見した端末を表示するかどうか設定する(optional)
// info: アドバタイズで設定されているのdiscoveryInfo
func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool

// Doneボタン押下時に呼ばれる(required)
func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController)

// Cancelボタン押下時に呼ばれる(required)
func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController)

こんな感じです。発見したものが一覧表示されタップすると招待を送り相手側で許可されると Connected になります。

browser

簡易実装

下記のようなチャットアプリを作成します。

chat

こんな感じです。

import UIKit
import MultipeerConnectivity

final class ChatTableViewController: UITableViewController {

    private var messages = [String]()

    private let serviceType = "sample-chat"
    private var session: MCSession!
    private var advertiser: MCNearbyServiceAdvertiser!
    private var browser: MCNearbyServiceBrowser!

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.rightBarButtonItem = .init(title: "送信", style: .done, target: self, action: #selector(sendMessage(_:)))
        navigationItem.rightBarButtonItem?.isEnabled = false

        let peerID = MCPeerID(displayName: UIDevice.current.name)
        session = MCSession(peer: peerID)
        session.delegate = self

        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        advertiser.delegate = self
        advertiser.startAdvertisingPeer()

        browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        browser.delegate = self
        browser.startBrowsingForPeers()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        // たまに切れない時があるのでここで切断
        browser.stopBrowsingForPeers()
        advertiser.stopAdvertisingPeer()
        session.disconnect()
    }

    @objc private func sendMessage(_ sender: Any) {
        let message = "\(session.myPeerID.displayName)からのメッセージ"
        do {
            try session.send(message.data(using: .utf8)!, toPeers: session.connectedPeers, with: .reliable)
            addMessage(message)
        } catch let error {
            print(error.localizedDescription)
        }
    }

    private func addMessage(_ message: String) {
        messages.append(message)
        tableView.beginUpdates()
        let indexPath = IndexPath(row: messages.count - 1, section: 0)
        tableView.insertRows(at: [indexPath], with: .automatic)
        tableView.endUpdates()
        tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
    }
}

extension ChatTableViewController {

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = messages[indexPath.row]
        return cell
    }
}

extension ChatTableViewController: MCSessionDelegate {

    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        let message: String
        switch state {
        case .connected:
            message = "\(peerID.displayName)が接続されました"
        case .connecting:
            message = "\(peerID.displayName)が接続中です"
        case .notConnected:
            message = "\(peerID.displayName)が切断されました"
        @unknown default:
            message = "\(peerID.displayName)が想定外の状態です"
        }
        DispatchQueue.main.async {
            self.addMessage(message)
            self.navigationItem.rightBarButtonItem?.isEnabled = !self.session.connectedPeers.isEmpty
        }
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        guard let message = String(data: data, encoding: .utf8) else {
            return
        }
        DispatchQueue.main.async {
            self.addMessage(message)
        }
    }

    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        assertionFailure("非対応")
    }

    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        assertionFailure("非対応")
    }
d
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        assertionFailure("非対応")
    }
}

extension ChatTableViewController: MCNearbyServiceAdvertiserDelegate {

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
    }
}

extension ChatTableViewController: MCNearbyServiceBrowserDelegate {

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        guard let session = session else {
            return
        }
        browser.invitePeer(peerID, to: session, withContext: nil, timeout: 0)
    }

    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
    }
}

1対1でやるだけなら接続した時点でアドバタイズと探索を停止させるべきかもしれない:thinking:

おまけ

sendResource を使えば下記のような画像の送受信もできます:v:

詳細はソースをどうぞ :point_right: GitHub

その他気になった点

アプリがバックグラウンドにいったとき

アプリがバックグラウンドになると、アドバタイズと検索を停止してセッションをすべて切断する模様。フォアグラウンドに復帰すると、自動的にアドバタイズと検索を再開するが、セッションの復帰はこちらでやる必要があるようです。

接続時のエラー

接続時に下記のようなログが表示されるが接続はできてる模様。

[GCKSession] Not in connected state, so giving up for participant [7AE9D215] on channel [0].

Wi-Fi必須?

Bluetooth で接続できるから端末の Wi-Fi 設定を OFF にしても接続できるのかと思いましたが Wi-Fi は ON にする必要があるようです(ソースは見つかってないです)。Wi-Fi の接続先は同一でなくてもいいようです。

Codableによるデータのやりとり

iOS12 と iOS13, 14 間で Codable のデータのやりとりができませんでした:frowning2:
下記のようにやってみると受信のところでエラーになりました(iOS13, 14 の場合は OK)。

enum Hoge: Int, Codable {
  case piyo
}

// 送信
func sendHoge(_ hoge: Hoge) {
    do {
        let data = try JSONEncoder().encode(hoge)
        try session.send(data, toPeers: session.connectedPeers, with: .reliable)
    } catch let error {
        print(error.localizedDescription)
    }
}

// 受信
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
    do {
        let hoge = try JSONDecoder().decode(Hoge.self, from: data)
    } catch let error {
        // ここにはいる
        // The given data was not valid JSON.
        print(error.localizedDescription)
    }
}

おわりに

さくっと端末間の接続ができるのはすばらしい:clap:

ちょっと調べてると過去 OS バージョンではバグが多そうでしたが最近は安定してるのかどうなんだろう:innocent:
iOS 12.4.9(iPod touch 第6世代)、iOS 13.7(iPad 第5世代)、iOS 14.0(シミュレータ)で接続できるのは確認しました。

手動接続とストリームに関してはよくわかりませんでした!

参考

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

Vision+CoreML+Metal で信号機の点灯色を読み取ってみる

色の表現としてHSV色空間というものに触れる機会があったので、信号機の色を読み取ってみた。
信号機の画像認識には YOLOv3-Tiny のCore MLモデルを利用。
<完成イメージ>
demo.png demo.gif
※このgif画像は外で撮影した動画をモニタで再生し、それをiPhoneで撮影。

「色」をどう読み取るのか 〜HSV色空間について〜

業務でアプリ開発をしていて色の指定にはRGBを使っていて、
たまに見かける↓での色表現(HSV色空間)は 何が嬉しいのか?? と思っていた。

HSV色空間とは(Wikipediaより)

HSVモデル(英: HSV model)は色相(Hue)、彩度(Saturation・Chroma)、明度(Value・Brightness)の三つの成分からなる色空間。

 嬉しいことは色だけに色々あるようで、0~360度の色の輪(色相環)で表現される「色相(Hue)」を利用することで「赤っぽい色」「青っぽい色」といった人間の認識に近い処理ができる。

 ちなみに、AppleのCIImageのフィルターのドキュメント 『Applying a Chroma Key Effect』 ではクロマキー合成のため緑っぽい背景色を消すのに色相の108~144度で判定している。

 で、HSVを使って何かやるなら信号機読み取ってみるか、 ということで信号機が何色に点灯しているのかの判別をしてみた。以下、作成したサンプルプログラムについて解説します。

信号機の点灯色の識別

色相

 点灯色は次の色相で判定(家の近所の3つの信号機、かつ、天気の良い日中で判定できるように指定したので汎用性はないです)。

点灯色 色相 備考
140〜220度 「青」といっても「緑」に近い場合がある
5〜60度 「黄」もかなり「赤」に踏み込まないと認識せず。
340〜5度 比較的安定して認識できる
彩度

 色相だけだと信号機の枠の部分とかのランプ以外のところが 若干の青み があるだけで、青信号と認識してしまうので「彩度(Saturation)」を使って、しっかりとした青色 を判定できるようにしている。

彩度 備考
80%以上 【彩度とは】
色の鮮やかさの尺度(0~100%)。
白・グレー・黒(つまり、R = G = B)は鮮やかではないので 0%。
R, G, B すべて0ではないが、0があるのとき彩度が 100%

<彩度による判定効果の実例>
色相環(ここで撮影した色相環は中央に行くについれて白っぽくなる=彩度が落ちる)をiPhoneで撮影して、青・黄・赤の各色相を単色でぬり潰す加工(それ以外の色は黒)を行い、彩度別に表示範囲を絞ったものがこちら。

彩度
指定なし
彩度
30%以上を表示
彩度
80%以上を表示
s_0.PNG s_30.PNG s_80.PNG

 彩度の指定がない場合、何かしらの色に分類されてしまい、この例では白い部分が黄色に判定されている。彩度の大きな部分だけ表示することで、より鮮やかな部分だけ残る、という効果が確認できる。

色の読み取りステップ

  1. カメラ画像をキャプチャする
  2. 画像からオブジェクト(信号機)を識別する
  3. 信号機部分の画像だけ切り取る
  4. 3.にブラーをかけて画像内の色のばらつきを抑える
  5. 4.の画像の青信号・黄信号・赤信号の色区別する
  6. 何色が点灯しているのか判定する

1. カメラ画像をキャプチャする

これはAppleの『YOLOv3-Tinyのサンプルプログラム』と同じ。一点、キャプチャした画像からあとで画像の切り出しがしやすいようにCMSampleBuffer から CGImage に変換しておく。

if let pb = CMSampleBufferGetImageBuffer(sampleBuffer) {
    // 検出した信号機をキャプチャー画像の上に重ねて表示させるためCGImageにしてとっておく
    let ciImage = CIImage(cvPixelBuffer: pb)
    let pixelBufferWidth = CGFloat(CVPixelBufferGetWidth(pixelBuffer))
    let pixelBufferHeight = CGFloat(CVPixelBufferGetHeight(pixelBuffer))
    let imageRect:CGRect = CGRect(x: 0,y: 0,width: pixelBufferWidth, height: pixelBufferHeight)
    let ciContext = CIContext.init()

    // Metal側でピクセルフォーマットを BGRA8 にしているので、CGImage作る時も合わせておく。
    cgimage = ciContext.createCGImage(ciImage, from: imageRect, format: .BGRA8, colorSpace: nil)
}

ポイントは、あとでMetalで加工する際のピクセルフォーマットを BGRA にしているので、CGImageを作る際にピクセルフォーマットを合わせているところ。

2. 画像からオブジェクト(信号機)を識別する

これも『YOLOv3-Tinyのサンプルプログラム』と同様。ここでは信号機以外の認識情報は捨てる。

let objectRecognition = VNCoreMLRequest(model: model, completionHandler: { (request, error) in
    DispatchQueue.main.async(execute: {
        self.cropedCGImage = nil

        guard let results = request.results else { return }
        for observation in results {
            guard let objectObservation = observation as? VNRecognizedObjectObservation,
                  objectObservation.labels[0].identifier == "traffic light" else {
                // 信号機以外の認識画像は捨てる
                continue
            }
// 略

3. 信号機部分の画像だけ切り取る

1.で取得しておいたキャプチャ画像から信号機部分の画像を切りだす。

// 信号機部分の画像だけ切り出す
let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, self.videoWidth, self.videoHeight)
self.cropedCGImage = self.cgimage?.cropping(to: objectBounds)

あと、Metalで加工するため、MTLTexture に変換しておく。

let textureLoader = MTKTextureLoader(device: device)
// CGImageからMTLTexture作る時はusage指定が必要。
let usage = MTLTextureUsage.renderTarget.rawValue | MTLTextureUsage.shaderRead.rawValue | MTLTextureUsage.shaderWrite.rawValue
let texture = try? textureLoader.newTexture(cgImage: cropped,
      options: [.textureUsage : NSNumber.init(value:usage)])

ポイントはMTKTextureLoaderによる CGImage->MTLTextureへの変換時に、usage を設定するところ。これを設定しないと「usageを設定しろ」と実行時におこられる。shaderRead はシェーダー側で読めるようにする、renderTarget は色の情報を取り扱えるようにする、という指定。shaderWrite は、このあとここで作成したMTLTextureに対し、Metal Performance Shadersでブラーをかけて書き込みをするので指定が必要。

4. 3.にブラーをかけて画像内の色のばらつきを抑える

このサンプルでの点灯色の判定方法は後述するが、画像上、いくつかの「点」の色を調べて、点灯色を判定している。LED信号機のように点が密集していたり、色のノイズがあると、判定精度が落ちるので、ブラーをかけて判定精度を上げる。

// LED信号機のつぶつぶを潰して信号判定精度を上げる
let blurKernek = MPSImageGaussianBlur(device: device, sigma: 2)
blurKernek.encode(commandBuffer: commandBuffer,
                  inPlaceTexture: &texture, fallbackCopyAllocator: nil)

ブラーの強さは適当。

5. 4.の画像の青信号・黄信号・赤信号の色区別する

ここはMetalのコンピュートシェーダーで処理。入力となるMTLTexture(信号機画像)から、色相・彩度により青信号は青(R,G,B=0,0,1)、黄信号は黄色(R,G,B=1,1,0)、赤信号は赤(R,G,B=1,0,0)に変換したMTLTextureを出力。

Shader.metal
// 色相・彩度に応じて、色を青・黄・青に塗りつぶす
kernel void colorClassify(texture2d<half, access::read> inTexture [[ texture(0) ]],
                          texture2d<half, access::write> outTexture [[ texture(1) ]],
                          uint2 gid [[thread_position_in_grid]]) {

    half3 color = inTexture.read(gid).rgb;

    half s = saturation(color.r, color.g, color.b);
    // 彩度が低い場合は判定NG
    if(!isGoodSaturation(s)) {
        outTexture.write(half4(0.0), gid);
        return;
    }

    half h = hue(color.r, color.g, color.b);
    if(isBlue(h)) {
        outTexture.write(half4(0.0, 0.0, 1.0, 1.0), gid);
        return;
    }
    if(isYellow(h)) {
        outTexture.write(half4(1.0, 1.0, 0.0, 1.0), gid);
        return;
    }
    if(isRed(h)) {
        outTexture.write(half4(1.0, 0.0, 0.0, 1.0), gid);
        return;
    }
    outTexture.write(half4(0.0), gid);
}

RGB→HSVへの変換は、こちらのサイト『RGBとHSVの相互変換[色見本/サンプル付き]』を参考にさせてもらいました。ちなみにwikiの「色相」を見るとこの変換方法は「大まかな値を求める」方法らしい。より正確な変換方法は、こちらのサイト『HSVの計算について』1 にある方法が良さそうだが、ちょっと面倒なのでスキップ。

ちなみに、
サンプルを作り始めたときは重い処理もあるだろうからと考えてMetalを使ったが、結局、識別した色を可視化するのに使っただけで、点灯色を把握するだけならMetalを使う必要なかった。可視化するにしても CIColorKernel がお手軽でよかったかも。

6. 何色が点灯しているのか判定する

ここが一番悩ましかった。手法は無数にあるような気がするが、この辺り詳しくないので、最低限、次を実現できる方法を考えた。

  • 処理が軽いこと。
  • 青空の影響を受けにくいこと。

で、実装したのは次のように信号機の縦中央・横に7箇所の色を取得して、色の多数決で点灯色を判定する、と言う方法(7箇所にしたのは適当)。
<判定箇所のイメージ>
imga.png

これなら青空の影響(青信号の誤判定)を避けやすく、かつ、判定処理も軽め。

// MTLTextureをUInt8の配列でアクセスできるようにする
let bytesPerPixel: Int = 4
let imageByteCount = changedTexture.width * changedTexture.height * bytesPerPixel
let bytesPerRow = changedTexture.width * bytesPerPixel
var src = [UInt8](repeating: 0, count: Int(imageByteCount))
let region = MTLRegionMake2D(0, 0, changedTexture.width, changedTexture.height)
classifiedTexture.getBytes(&src, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)

// 画像の(見た目の)縦中央の画素を、右から左に7箇所等間隔で色を取得し、多数決で点灯色を判定
var signalCount: [(UIColor, Int)] = [(.blue, 0), (.yellow, 0), (.red, 0)]
let step = changedTexture.height / 8
for y in stride(from: step, to: changedTexture.height - step, by: step) {
    // 【注意】①BGRAの順番に色があること ②横向き画像であるといこと
    let baseIndex = (y * changedTexture.width + (changedTexture.width / 2)) * bytesPerPixel
    if src[baseIndex + 0] != 0 {
        signalCount[0].1 += 1   // 青+1
    } else if src[baseIndex + 1] != 0 {
        signalCount[1].1 += 1   // 黄+1 ※黄色=赤+緑なので赤より先に判定すること。
    } else if src[baseIndex + 2] != 0 {
        signalCount[2].1 += 1   // 赤+1
    }
}

なお、MTLTextureから画素を取り出す方法については、こちらの記事『Metalシェーディング言語を使用したUIImageの加工』を参考にさせていただきました。

あと、上記の方法だけだと、信号機の画像認識ができなかったり、色のノイズで誤判定されたり、信号機を撮影するとフリッカーがでたり、で安定して認識できなかったので、過去10回の個別の点灯色の認識結果の中で一番多く認識できた色=点灯色、という判定も入れた。

if signalHistory.count > 10 {
    // 過去の判定結果は10個まで
    signalHistory.removeFirst()
}

var signalCount: [(UIColor, Int)] = [(.blue, 0), (.yellow, 0), (.red, 0)]
signalHistory.forEach { signal in
    switch signal {
    case .blue:   signalCount[0].1 += 1
    case .yellow: signalCount[1].1 += 1
    case .red:    signalCount[2].1 += 1
    default: break
    }
}

if let signal = signalCount.max(by: { $0.1 < $1.1 }) {
    return signal.0
} else {
    return .clear
}

いろいろと条件が整っているときには点灯色は読み取れている。
(安定して認識できないケースは多々ある。最後に課題を記載)

最後に

目的がHSV色空間を使って何かする、、なので目的は達成したが、真剣に信号機の色を読み取るとなると課題は多い。思いつく課題は次の通り。

  1. そもそも信号機の画像認識精度がよくない(YOLOv3-Tinyは日本の信号機で学習されてないのでしょう)。日本の信号機を使って、信号機だけで学習すれば改善されそう。
  2. 色の判定を、ベタに色で判定した。これだと、夜とか、逆光とか撮影条件による影響が大きいように思われる。撮影条件×各色を含めた信号機の機械学習されたモデルがあれば、色で判定しなくていいかもしれない。画像判定前(&トレーニングデータ画像)に適切な前処理は必要そう。
  3. 撮影すると信号機って結構小さい。信号機を識別させるにはより信号機が大きく見える条件付けが必要かも。例えば、画像の上半分にしか信号機は存在しない前提を置くとか。

全体ソースコード

ViewController.swift
import UIKit
import AVFoundation
import Vision
import Metal
import MetalKit
import MetalPerformanceShaders

class ViewController: UIViewController {

    @IBOutlet weak var preview: UIView!
    @IBOutlet weak var lblColor: UILabel!

    // キャプチャ画像サイズ
    private var videoWidth = 0
    private var videoHeight = 0
    // キャプチャ画像の入出力
    private var rootLayer: CALayer! = nil
    private let session = AVCaptureSession()
    private let videoDataOutput = AVCaptureVideoDataOutput()
    private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput",
                                                     qos: .userInitiated,
                                                     attributes: [],
                                                     autoreleaseFrequency: .workItem)
    private var previewLayer: AVCaptureVideoPreviewLayer! = nil

    private var request: VNCoreMLRequest?
    // 信号機画像を加工した画像を乗せるレイヤー
    private var detectionOverlay: CALayer! = nil
    private var cgimage: CGImage?
    // Metal
    private let device = MTLCreateSystemDefaultDevice()!
    private lazy var commandQueue = device.makeCommandQueue()!
    // 描画用
    private var pipelineState: MTLRenderPipelineState!
    private var imagePlaneVertexBuffer: MTLBuffer!
    private var timer: CADisplayLink!
    private var metalLayer: CAMetalLayer!
    // 色分類用
    private var colorClassComputeState: MTLComputePipelineState!
    private var threadgroupSize = MTLSizeMake(16, 16, 1)
    private var classifiedTexture: MTLTexture!
    // Metal 画像頂点座標(x,y), uv座標(u,v)
    private let kImagePlaneVertexData: [Float] = [
        -1.0, -1.0, 0.0, 1.0,
        1.0, -1.0, 1.0, 1.0,
        -1.0, 1.0, 0.0, 0.0,
        1.0, 1.0, 1.0, 0.0
    ]
    // 信号機部分を切り出した画像
    var cropedCGImage: CGImage?
    // 点灯色の切り出し記録。チラつき抑止のために使う。
    var signalHistory: [UIColor] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        setupAVCapture()
        setupLayers()
        setupVision()
        setupMetal()
        self.view.bringSubviewToFront(lblColor)
        // キャプチャ開始
        session.startRunning()
        // CAMetalLayerを使うので描画タイミングを生成
        timer = CADisplayLink(target: self, selector: #selector(ViewController.drawLoop))
        timer.add(to: RunLoop.main, forMode: .default)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 画像認識は 640x480 で行うのでLayer全体を右90度回転する
        let bounds = rootLayer.bounds
        let xScale = bounds.size.width / CGFloat(videoHeight)
        let yScale = bounds.size.height / CGFloat(videoWidth)
        let scale = fmax(xScale, yScale)
        detectionOverlay.setAffineTransform(CGAffineTransform(rotationAngle: CGFloat(.pi / 2.0)).scaledBy(x: scale, y: scale))
        detectionOverlay.position = CGPoint(x: bounds.midX, y: bounds.midY)
    }

    // CAMetalLayerに信号機画像を描画
    @objc func drawLoop() {
        guard let cropped = cropedCGImage else {
            metalLayer.isHidden = true
            return
        }
        metalLayer.isHidden = false
        autoreleasepool {
            let textureLoader = MTKTextureLoader(device: device)
            // CGImageからMTLTexture作る時はusage指定が必要。
            let usage = MTLTextureUsage.renderTarget.rawValue | MTLTextureUsage.shaderRead.rawValue | MTLTextureUsage.shaderWrite.rawValue
            let texture = try? textureLoader.newTexture(cgImage: cropped,
                  options: [.textureUsage : NSNumber.init(value:usage)])

            // 切り抜いた信号機画像を点灯色が目立つように加工
            guard let changedTexture = changeColor(texture: texture) else {
                // 画像変換に失敗しているので点灯色識別結果に青・黄・赤以外を設定
                signalHistory.append(.clear)
                return
            }

            // MTLTextureをUInt8の配列でアクセスできるようにする
            let bytesPerPixel = 4
            let imageByteCount = changedTexture.width * changedTexture.height * bytesPerPixel
            let bytesPerRow = changedTexture.width * bytesPerPixel
            var src = [UInt8](repeating: 0, count: Int(imageByteCount))
            let region = MTLRegionMake2D(0, 0, changedTexture.width, changedTexture.height)
            classifiedTexture.getBytes(&src, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)

            // 画像の(見た目の)縦中央の画素を、右から左に7箇所等間隔で色を取得し、多数決で点灯色を判定
            var signalCount: [(UIColor, Int)] = [(.blue, 0), (.yellow, 0), (.red, 0)]
            let step = changedTexture.height / 8
            for y in stride(from: step, to: changedTexture.height - step, by: step) {
                // 【注意】①BGRAの順番に色があること ②横向き画像であるといこと
                let baseIndex = (y * changedTexture.width + (changedTexture.width / 2)) * bytesPerPixel
                if src[baseIndex + 0] != 0 {
                    signalCount[0].1 += 1   // 青+1
                } else if src[baseIndex + 1] != 0 {
                    signalCount[1].1 += 1   // 黄+1 ※黄色=赤+緑なので赤より先に判定すること。
                } else if src[baseIndex + 2] != 0 {
                    signalCount[2].1 += 1   // 赤+1
                }
            }

            if let signal = signalCount.max(by: { $0.1 < $1.1 }), signal.1 > 0 {
                signalHistory.append(signal.0)
            } else {
                signalHistory.append(.clear)
            }

            // 過去10回の点灯色認識結果の中で一番多い点灯色を採用
            let detectedColor = detectedColorWithouitNoise()
            lblColor.text = detectedColor.jaText
            lblColor.backgroundColor = detectedColor
            lblColor.textColor = detectedColor.textColor

            // 加工済み信号機画像を描画
            drawSignal()
        }
    }

    private func detectedColorWithouitNoise() -> UIColor {
        if signalHistory.count > 10 {
            // 過去の判定結果は10個まで
            signalHistory.removeFirst()
        }

        var signalCount: [(UIColor, Int)] = [(.blue, 0), (.yellow, 0), (.red, 0)]
        signalHistory.forEach { signal in
            switch signal {
            case .blue:   signalCount[0].1 += 1
            case .yellow: signalCount[1].1 += 1
            case .red:    signalCount[2].1 += 1
            default: break
            }
        }

        if let signal = signalCount.max(by: { $0.1 < $1.1 }) {
            return signal.0
        } else {
            return .clear
        }
    }

    private func changeColor(texture: MTLTexture?) -> MTLTexture? {
        guard var texture = texture else { return nil }
        let commandBuffer = self.commandQueue.makeCommandBuffer()!

        // LED信号機のつぶつぶを潰して信号判定精度を上げる
        let blurKernek = MPSImageGaussianBlur(device: device, sigma: 2)
        blurKernek.encode(commandBuffer: commandBuffer,
                          inPlaceTexture: &texture, fallbackCopyAllocator: nil)

        // 色相・彩度により信号機画像を青・黄・赤の単色に塗りつぶす
        let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
        let colorDesc = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm,
                                                                 width: texture.width, height: texture.height, mipmapped: false)
        colorDesc.usage = [.shaderRead, .shaderWrite]
        classifiedTexture = device.makeTexture(descriptor: colorDesc)

        let threadCountW = (texture.width + self.threadgroupSize.width - 1) / self.threadgroupSize.width
        let threadCountH = (texture.height + self.threadgroupSize.height - 1) / self.threadgroupSize.height
        let threadgroupCount = MTLSizeMake(threadCountW, threadCountH, 1)

        computeEncoder.setComputePipelineState(colorClassComputeState)
        computeEncoder.setTexture(texture, index: 0)
        computeEncoder.setTexture(classifiedTexture, index: 1)
        computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
        computeEncoder.endEncoding()
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()

        return classifiedTexture
    }

    private func drawSignal() {
        guard let drawable = metalLayer.nextDrawable() else { return  }

        let commandBuffer = self.commandQueue.makeCommandBuffer()!

        let renderPassDescriptor = MTLRenderPassDescriptor()
        renderPassDescriptor.colorAttachments[0].texture = drawable.texture
        renderPassDescriptor.colorAttachments[0].loadAction = .clear
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0)

        let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
        renderEncoder.setCullMode(.none)
        renderEncoder.setRenderPipelineState(pipelineState)
        renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 0)
        renderEncoder.setFragmentTexture(classifiedTexture, index: 0)
        renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
        renderEncoder.endEncoding()

        commandBuffer.present(drawable)
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()
    }
}

extension ViewController {

    private func setupAVCapture() {
        let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                           mediaType: .video,
                                                           position: .back).devices.first
        guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice!) else { return }
        // capture セッション セットアップ
        session.beginConfiguration()
        session.sessionPreset = .vga640x480

        // 入力デバイス指定
        session.addInput(deviceInput)

        // 出力先の設定
        session.addOutput(videoDataOutput)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
        let captureConnection = videoDataOutput.connection(with: .video)
        captureConnection?.isEnabled = true

        // ビデオの画像サイズ取得
        try? videoDevice!.lockForConfiguration()
        let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
        videoWidth = Int(dimensions.width)
        videoHeight = Int(dimensions.height)
        videoDevice!.unlockForConfiguration()

        session.commitConfiguration()

        // プレビューセットアップ
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        rootLayer = preview.layer
        previewLayer.frame = rootLayer.bounds
        rootLayer.addSublayer(previewLayer)
    }

    private func setupVision() {
        guard let model = try? VNCoreMLModel(for: YOLOv3Tiny(configuration: MLModelConfiguration()).model) else { return }

        let objectRecognition = VNCoreMLRequest(model: model, completionHandler: { (request, error) in
            DispatchQueue.main.async(execute: {
                self.cropedCGImage = nil

                guard let results = request.results else { return }
                for observation in results {
                    guard let objectObservation = observation as? VNRecognizedObjectObservation,
                          objectObservation.labels[0].identifier == "traffic light" else {
                        // 信号機以外の認識画像は捨てる
                        continue
                    }

                    // 信号機部分の画像だけ切り出す
                    let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, self.videoWidth, self.videoHeight)
                    self.cropedCGImage = self.cgimage?.cropping(to: objectBounds)
                    // 信号機画像(加工済み)を表示する位置を設定する
                    self.metalLayer.bounds = objectBounds
                    self.metalLayer.position = CGPoint(x: objectBounds.midX, y: objectBounds.midY)
                    break
                }
            })
        })
        request = objectRecognition
    }

    private func setupLayers() {
        // CAMetalLayerを乗せるレイヤー。あとで全体を90度回転して使う。
        detectionOverlay = CALayer()
        detectionOverlay.bounds = CGRect(x: 0.0,
                                         y: 0.0,
                                         width: CGFloat(videoWidth),
                                         height: CGFloat(videoHeight))
        detectionOverlay.position = CGPoint(x: rootLayer.bounds.midX, y: rootLayer.bounds.midY)
        rootLayer.addSublayer(detectionOverlay)
        // Metalで作ったMTLTextureの描画レイヤー
        metalLayer = CAMetalLayer()
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        detectionOverlay.addSublayer(metalLayer)
    }

    private func setupMetal() {
        // 頂点座標バッファ確保&頂点情報流し込み
        let vertexDataCount = kImagePlaneVertexData.count * MemoryLayout<Float>.size
        imagePlaneVertexBuffer = device.makeBuffer(bytes: kImagePlaneVertexData, length: vertexDataCount, options: [])
        // シェーダー指定
        let defaultLibrary = device.makeDefaultLibrary()!
        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.vertexFunction = defaultLibrary.makeFunction(name: "imageVertex")!
        pipelineStateDescriptor.fragmentFunction = defaultLibrary.makeFunction(name: "imageFragment")!
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        pipelineStateDescriptor.sampleCount = 1

        try? pipelineState = device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)

        // 画像分類色設定
        let labelConvertShader = defaultLibrary.makeFunction(name: "colorClassify")!
        colorClassComputeState = try! self.device.makeComputePipelineState(function: labelConvertShader)
    }
}

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // 信号機画像の検出開始
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
                                                        orientation: .up,
                                                        options: [:])
        guard let request = self.request else { return }
        try? imageRequestHandler.perform([request])

        // 検出した信号機をキャプチャー画像の上に重ねて表示させるためCGImageにしてとっておく
        guard let pb = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        let ciImage = CIImage(cvPixelBuffer: pb)
        let pixelBufferWidth = CGFloat(CVPixelBufferGetWidth(pixelBuffer))
        let pixelBufferHeight = CGFloat(CVPixelBufferGetHeight(pixelBuffer))
        let imageRect:CGRect = CGRect(x: 0,y: 0,width: pixelBufferWidth, height: pixelBufferHeight)
        let ciContext = CIContext.init()

        // Metal側でピクセルフォーマットを BGRA8 にしているので、CGImage作る時も合わせておく。
        cgimage = ciContext.createCGImage(ciImage, from: imageRect, format: .BGRA8, colorSpace: nil)
    }
}

extension UIColor {
    var jaText: String {
        switch self {
        case .blue: return "青"
        case .yellow: return "黄"
        case .red: return "赤"
        default: return ""
        }
    }

    var textColor: UIColor {
        switch self {
        case .blue: return .white
        case .yellow: return .black
        case .red: return .white
        default: return .black
        }
    }
}
Shader.metal
#include <metal_stdlib>
using namespace metal;

typedef struct {
    float2 position;
    float2 texCoord;
} ImageVertex;

typedef struct {
    float4 position [[position]];
    float2 texCoord;
} ColorInOut;

vertex ColorInOut imageVertex(const device ImageVertex* vertices [[ buffer(0) ]],
                              unsigned int vid [[ vertex_id ]]) {
    ColorInOut out;
    const device ImageVertex& cv = vertices[vid];
    out.position = float4(cv.position, 0.0, 1.0);
    out.texCoord = cv.texCoord;
    return out;
}

// rgb -> 色相変換
half hue(half r, half g, half b) {
    half rgbMax = max3(r, g, b);
    half rgbMin = min3(r, g, b);
    if(rgbMax == rgbMin) {
        return 0;
    } else {
        half hue;
        if(rgbMax == r) {
            hue = 60.0 * (g - b) / (rgbMax - rgbMin);
        } else if(rgbMax == g) {
            hue = 60.0 * (b - r) / (rgbMax - rgbMin) + 120;
        } else {
            hue = 60.0 * (r - g) / (rgbMax - rgbMin) + 240;
        }
        if(hue < 0) {
            hue += 360;
        }
        return hue;
    }
}

// rgb -> 彩度変換
half saturation(half r, half g, half b) {
    half rgbMax = max3(r, g, b);
    half rgbMin = min3(r, g, b);

    if(rgbMax == rgbMin) {
        return 0;
    } else {
        return (rgbMax - rgbMin) / rgbMax;
    }
}

bool isBlue(half hue) {
    return hue > 140 && hue < 220;
}

bool isYellow(half hue) {
    return hue > 5 && hue < 60;
}

bool isRed(half hue) {
    return hue < 5 || hue > 340;
}

bool isGoodSaturation(half saturation) {
    return saturation > 0.8;
}

fragment half4 imageFragment(ColorInOut in [[ stage_in ]],
                             texture2d<half, access::sample> signalTexture [[ texture(0) ]]) {
    constexpr sampler colorSampler(mip_filter::linear,
                                   mag_filter::linear,
                                   min_filter::linear);

    half4 color = signalTexture.sample(colorSampler, in.texCoord.xy);
    return color;
}

// 色相・彩度に応じて、色を青・黄・青に塗りつぶす
kernel void colorClassify(texture2d<half, access::read> inTexture [[ texture(0) ]],
                          texture2d<half, access::write> outTexture [[ texture(1) ]],
                          uint2 gid [[thread_position_in_grid]]) {

    half3 color = inTexture.read(gid).rgb;

    half s = saturation(color.r, color.g, color.b);
    // 彩度が低い場合は判定NG
    if(!isGoodSaturation(s)) {
        outTexture.write(half4(0.0), gid);
        return;
    }

    half h = hue(color.r, color.g, color.b);
    if(isBlue(h)) {
        outTexture.write(half4(0.0, 0.0, 1.0, 1.0), gid);
        return;
    }
    if(isYellow(h)) {
        outTexture.write(half4(1.0, 1.0, 0.0, 1.0), gid);
        return;
    }
    if(isRed(h)) {
        outTexture.write(half4(1.0, 0.0, 0.0, 1.0), gid);
        return;
    }
    outTexture.write(half4(0.0), gid);
}

参考URL


  1. 最初、wikiにあるatan()使う方法で作り始めたが正しく変換ができないのに気がついて、このサイトに辿り着いた 

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

みんなのiOS講座 ゼロからSwiftで学ぶiPhoneアプリ開発の基礎:作業メモ③

本日もみんなのiOS講座 ゼロからSwiftで学ぶiPhoneアプリ開発の基礎の学んだことメモ。
Swiftの基礎nの続き。

分岐

ifを使った条件分岐。
ここはSwift Playgroundsでやっていたこともあったので、そんなに難しくなかった。

配列

  • 配列を用いることで、1つの変数の中に複数の要素を格納することができる。
let a = [1, 2, 3, 4, 5]

上記の場合、変数が全て整数なので、int型の変数だと認識される。

  • また、下記のように[]を使って数字を指定すると、配列の中の各要素を取り出すことができる。
let b = a[0]

この場合、aの配列の先頭は1という数字なので、1という数字が出力される。

  • letの場合、配列の中を変更することができないが、varの場合は配列の中を変更することができる。
var e = [1, 2, 3, 4, 5]

e.append(6)
e.remove(at: 3)

この場合、appendで6という要素が新たに追加されたことになり、removeで先頭から3番目の要素が削除された状態となる。

  • 加えて、下記のように配列の要素を指定して、変更することもできる。
e[2] = 9

この場合、先頭から2番目の数字が9に変更されている。

ディクショナリ

  • キーと値をセットで格納し、キーを使って値を取り出すことができる。
  • 多くのデータを効率的に管理するのにとても便利。
let a = ["Taro": 1985, "Hanako": 1986]

let b = a["Taro"]
let c = a["Hanako"]

この場合、Taroの値1985とHanakoの値1986が出力される。

  • varの場合は、ディクショナリを追加・変更することもできる。
var d: [String: Int] = ["Taro": 1985, "Hanako": 1986]
d["Jiro"] = 1988

この時はTaro、Hanakoに加えてJiroを追加した。既にキーが存在している場合は変更されることになる。

ループ

  • 同じような処理を何度も実行することができる。
for i in 0..<10 {
    print(i)
}
//10未満

for i in 0...10{
    print(i)
}
//10以下
  • forループと配列の組み合わせ
var a: [Int] = []
for i in 0..<10 {
    a.append(i)
}

varで空の配列を作り、for分を使って要素を入れることができる。
また、下記のように書くこともできる。

for i in a {
    print(i)
}

(実際に活用するときの具体的なイメージがわいていないので、違いがあまりわからない...)

ループと分岐の組み合わせ

var a: [Int] = []
for i in 0..<10 {
    if i%2 == 0 {
        a.append(i)
    }
}

この場合は2で割り切れる数字がiの配列のとして出力される([0, 2, 4, 6, 8])。

var b :[Int] = []
for i in 0..<10 {
    if i%3 == 0 {
        b.append(i*3)
    }else{
        b.append(i)
    }
}

この場合は、3で割り切れる数字だけ3倍されていて、他の数字は普通に出力される([0, 1, 2, 9, 4, 5, 18, 7, 8, 27])。

関数とスコープ

  • 関数を書いておくことで、処理をまとめておくことができるのでそれを何度も再利用することができる。
  • 関数名()をかけば、使用することができる
func a() {
    print("Hello!")
}
a()
  • スコープ

下記の例のように、関数の外で定義された変数の値に関数の中でアクセスできる。
関数の外で定義された変数に関しては、関数の中で使用できる。このような変数の有効範囲のことをスコープという。
また、関数の中で定義した変数は、関数の外からアクセスすることができない。

let b = 123
func c() {
    if b == 123 {
        print("Hi!")
    }
}
print(e)
//↑エラーが起こる

引数と返り値

  • 関数の外部から渡される値を引数という。
let a = 4
let b = 6

func add1(c: Int, d: Int) {
    let e = c + d
    print(e)
}
add1(c: a, d: b)
  • 返り値とは関数を実行した結果、得られる値
  • 返り値の方は->の後に記述する。
  • 返り値は関数の外に出ていく値となる。
func add2(c: Int, d: Int) -> Int {
    let e = c + d
    return e
}
let f = add2(c: a, d: b)
print(f)

メモ

なんとなく、何をやっているのかは理解しているが、アプリを作るときにどう使用するのか全くイメージがわいていない。
引き続き授業を続けて、解像度上げていきたい。

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

【Swift】AtCoder Beginner Contest 192

AtCoder Beginner Contest 192

下記リンクになります。
https://atcoder.jp/contests/abc192

A Star

問題
https://atcoder.jp/contests/abc192/tasks/abc192_a

import Foundation

func readInts(separate: String = " ") -> [Int] {
    readLine()!.components(separatedBy: " ").map { Int($0)! }
}

// MARK: - Main
func main() {
    let X = readInts()
    print(100-(X[0] % 100))
}

// MARK: - Run
main()

入力値Xを100で割った余りを100から引くことによって解を求める。

B uNrEaDaBlE sTrInG

問題
https://atcoder.jp/contests/abc192/tasks/abc192_b

func main() {
    let S: String = readLine()!
    var count = 1
    var isUnreadableString = true
    for string in S {
        let character: Character = string
        if count % 2 == 0 {
            if !character.isUppercase {
                isUnreadableString = false
            }
        } else {
            if !character.isLowercase {
                isUnreadableString = false
            }
        }
        count = count + 1
    }
    if isUnreadableString {
        print("Yes")
    } else {
        print("No")
    }
}

// MARK: - Run
main()

C Kaprekar Number

問題
https://atcoder.jp/contests/abc192/tasks/abc192_c

// 大きい順にソート
func g1(_ n: [String]) -> Int {
    let tmp: [String] = n.sorted(by: >)
    return Int(tmp.joined())!
}

// 小さい順にソート
func g2(_ n: [String]) -> Int {
    let tmp: [String] = n.sorted()
    return Int(tmp.joined())!
}

// 差分求める
func f(_ n: Int) -> Int {
    let nStr: [String] = String(n).compactMap(String.init)
    return g1(nStr) - g2(nStr)
}


// 入力
let NK: [Int] = readLine()!.split(separator: " ").map { Int($0)! }
let (N, K) = (NK[0], NK[1])

// main
var an = N
for _ in 0..<K {
    an = f(an)
}
print(an)

昇順、降順でソートした値の差分を求める問題。
その差分を再度昇順降順にソートして、差分を求めるをK回繰り返した値を出力。

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

プルリクエストについて注意事項をまとめてみる

swiftを教えていただいているメンターの方にプルリクの際の注意事項を熱く指導していただいたので、ここに残しておこうと思う。

・ファイル名、フォルダ名にスペースや全角文字は絶対に使わないこと
・変更したら必ず動作確認すること
・issueと関係ない差分入れないこと
・差分は他人に見せる前に自分で確認すること
・余計な差分があっても「Xcodeが勝手に変えたから」は通用しない、そのツールを選んで使ってるのは自分なので変更に責任があるのも自分、という自覚をもつこと
・差分の全てに「なぜ変えたのか」説明できること
・修正内容の確認のやり方が複雑なときはレビューしてもらう人にどうすれば確認できるのか伝えること

最初の「ファイル名、フォルダ名にスペースや全角文字は絶対に使わないこと」という初歩的な点が分からなかった愚か者だったので詳しく記載する。

・スペースはターミナルなどコマンドラインインタフェースで区切り文字扱いなのでバグが入り込むのでNG!

・全角文字は全角を扱えるサーバーじゃないと文字化けして死ぬのでNG!

・全角文字は日本でしか使われていないので、世界中のどこのサーバーでどう動くか分からない現代のインターネットに繋がったプログラムでは超ご法度。

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