- 投稿日:2021-02-21T23:11:22+09:00
【Swift】Compact状態のDatePickerに戻る処理を行う
新しいDatePicker
iOS14からどうやらDatePickerの挙動が変わったらしく、今までのドラムロール式の日付・日時選択からカレンダー表示の様な選択方法が選べるようになりました(従来のDatePickerも引き続き使用出来る)。
しかしこのポップアップされるDatePickerは日付を選択時に一つ前に戻る機能が備わって居らず自分で書いて選択時に戻る処理を入れてあげる必要がある。
コード
ViewController@IBAction func CompactDatePicker(_ sender: UIDatePicker) { // 日付選択時に選択前の画面に戻る self.dismiss(animated: true, completion: nil) }
- 投稿日:2021-02-21T21:47:58+09:00
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.
- 投稿日:2021-02-21T20:00:48+09:00
Multipeer ConnectivityでのP2P通信(Swift)
はじめに
iOS の端末間で通信とかしたいなと思い Multipeer Connectivity について調べました。
ソース
GitHub
Multipeer Connectivityとは
Multipeer Connectivity は Wi-Fi と Bluetooth を使って iOS 端末間の近距離通信(ピア・ツー・ピア)ができるライブラリです(最大8個まで接続できる)。
Wi-Fi が OFF でも Bluetooth で接続できるのかと思いましたが、OFF だとアドバタイザーが見えないらしく ON にする必要があるようです(同一 Wi-Fi でなくても OK)。接続の概要
接続の流れは下記のようになっています。
- MCPeerID を生成する。
端末を識別する一意のもの- MCSession を生成する。
接続を管理するやつ- MCNearbyServiceAdvertiser もしくは MCAdvertiserAssistant でアドバタイズする。
他の端末から検知できるようにする- MCNearbyServiceBrowser もしくは MCBrowserViewController で接続できる端末を探す。
- 発見した端末に対して招待を送る。
- 招待を受けた端末側で招待を許可すると接続が確立する。
- MCSession でデータの送受信を行う。
データ受信は MCSessionDelegate を使う各クラスの使い方
MCPeerID
識別用の一意のもの。下記イニシャライザで
displyaName(63 B 以下で UTF-8)を設定して生成する。
同名のものを設定しても別々の ID が生成される。空文字など異常値が設定されるとクラッシュする。init(displayName: String)アドバタイズや探索のたびに生成するのではなく同じものを利用するようにした方がよい(仮に advertiser と borowser に別の ID を設定すると自身が検知されてしまう)。
MCSession
下記どちらかで生成する。
// identity: 証明書関連で使う(ちょっとわからない。。。) // encryptionPreference: 通信を暗号化するかどうか init(peer myPeerID: MCPeerID, securityIdentity identity: [Any]?, encryptionPreference: MCEncryptionPreference) // encryptionPreferenceはrequiredになる init(peer myPeerID: MCPeerID)
encryptionPreferenceは下記がある。
- none: 暗号化しない
- required: 暗号化する
- optional: すべての端末が
requiredかoptionalであれば暗号化する。
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
下記で生成する。
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 を使えば接続できました。
下記で生成する。
// 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
細かい設定がいらない場合は 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
簡易に検索と画面表示をおこなってくれる
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 になります。
簡易実装
下記のようなチャットアプリを作成します。
こんな感じです。
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でやるだけなら接続した時点でアドバタイズと探索を停止させるべきかもしれない
おまけ
sendResourceを使えば下記のような画像の送受信もできます画像の送受信できた? pic.twitter.com/aTIdNVzQU7
— am10 (@am103141592) February 20, 2021詳細はソースをどうぞ
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のデータのやりとりができませんでした![]()
下記のようにやってみると受信のところでエラーになりました(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) } }おわりに
さくっと端末間の接続ができるのはすばらしい
ちょっと調べてると過去 OS バージョンではバグが多そうでしたが最近は安定してるのかどうなんだろう
![]()
iOS 12.4.9(iPod touch 第6世代)、iOS 13.7(iPad 第5世代)、iOS 14.0(シミュレータ)で接続できるのは確認しました。手動接続とストリームに関してはよくわかりませんでした!
参考
- 投稿日:2021-02-21T17:35:21+09:00
Vision+CoreML+Metal で信号機の点灯色を読み取ってみる
色の表現としてHSV色空間というものに触れる機会があったので、信号機の色を読み取ってみた。
信号機の画像認識には YOLOv3-Tiny のCore MLモデルを利用。
<完成イメージ>
![]()
※この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%以上を表示彩度の指定がない場合、何かしらの色に分類されてしまい、この例では白い部分が黄色に判定されている。彩度の大きな部分だけ表示することで、より鮮やかな部分だけ残る、という効果が確認できる。
色の読み取りステップ
- カメラ画像をキャプチャする
- 画像からオブジェクト(信号機)を識別する
- 信号機部分の画像だけ切り取る
- 3.にブラーをかけて画像内の色のばらつきを抑える
- 4.の画像の青信号・黄信号・赤信号の色区別する
- 何色が点灯しているのか判定する
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箇所にしたのは適当)。
<判定箇所のイメージ>
これなら青空の影響(青信号の誤判定)を避けやすく、かつ、判定処理も軽め。
// 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色空間を使って何かする、、なので目的は達成したが、真剣に信号機の色を読み取るとなると課題は多い。思いつく課題は次の通り。
- そもそも信号機の画像認識精度がよくない(YOLOv3-Tinyは日本の信号機で学習されてないのでしょう)。日本の信号機を使って、信号機だけで学習すれば改善されそう。
- 色の判定を、ベタに色で判定した。これだと、夜とか、逆光とか撮影条件による影響が大きいように思われる。撮影条件×各色を含めた信号機の機械学習されたモデルがあれば、色で判定しなくていいかもしれない。画像判定前(&トレーニングデータ画像)に適切な前処理は必要そう。
- 撮影すると信号機って結構小さい。信号機を識別させるにはより信号機が大きく見える条件付けが必要かも。例えば、画像の上半分にしか信号機は存在しない前提を置くとか。
全体ソースコード
ViewController.swiftimport 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
- HSV色空
- 色相
- HSVの計算について
- [RGBとHSVの相互変換色見本/サンプル付き
最初、wikiにあるatan()使う方法で作り始めたが正しく変換ができないのに気がついて、このサイトに辿り着いた ↩
- 投稿日:2021-02-21T11:12:21+09:00
みんなの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)メモ
なんとなく、何をやっているのかは理解しているが、アプリを作るときにどう使用するのか全くイメージがわいていない。
引き続き授業を続けて、解像度上げていきたい。
- 投稿日:2021-02-21T10:52:13+09:00
【Swift】AtCoder Beginner Contest 192
AtCoder Beginner Contest 192
下記リンクになります。
https://atcoder.jp/contests/abc192A Star
問題
https://atcoder.jp/contests/abc192/tasks/abc192_aimport 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_bfunc 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回繰り返した値を出力。
- 投稿日:2021-02-21T09:44:41+09:00
プルリクエストについて注意事項をまとめてみる
swiftを教えていただいているメンターの方にプルリクの際の注意事項を熱く指導していただいたので、ここに残しておこうと思う。
・ファイル名、フォルダ名にスペースや全角文字は絶対に使わないこと
・変更したら必ず動作確認すること
・issueと関係ない差分入れないこと
・差分は他人に見せる前に自分で確認すること
・余計な差分があっても「Xcodeが勝手に変えたから」は通用しない、そのツールを選んで使ってるのは自分なので変更に責任があるのも自分、という自覚をもつこと
・差分の全てに「なぜ変えたのか」説明できること
・修正内容の確認のやり方が複雑なときはレビューしてもらう人にどうすれば確認できるのか伝えること最初の「ファイル名、フォルダ名にスペースや全角文字は絶対に使わないこと」という初歩的な点が分からなかった愚か者だったので詳しく記載する。
・スペースはターミナルなどコマンドラインインタフェースで区切り文字扱いなのでバグが入り込むのでNG!
・全角文字は全角を扱えるサーバーじゃないと文字化けして死ぬのでNG!
・全角文字は日本でしか使われていないので、世界中のどこのサーバーでどう動くか分からない現代のインターネットに繋がったプログラムでは超ご法度。










