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

Nearby InteractionとARKitを使った高精度の距離計測と空間へのPlane配置

Nearby InteractionとARKitを使ったサンプルを実装しました。 Nearby Interactionの処理から取得できる数値を元に、1m離れるごとに空間上に板を自動配置しています。 iPhone11 ProとiPhone13 Proの2台の端末を使用。 どちらの端末もU1チップを搭載しており、高精度の距離測定が可能です。 GitHubにアップしています。 https://github.com/satoshi0212/AR_100Days/tree/main/AR_100Days/Days/Day59 この実装含め、仮想カメラ/AR/映像表現などの情報更新をTwitterで投稿しています。 良かったらフォローお願いします。 https://twitter.com/shmdevelop 実装ポイント iPhone11 ProとiPhone13 Proに同じアプリを入れ起動。 iPhone11 Proを机に置き、iPhone13 Proを手に持って離れていきます。 WiFiは不要(のはず)。 MultipeerConnectivityとNearby Interactionの2者が登場するため少し流れが複雑ですが、一度動かすと理解できると思います。 MultipeerConnectivityの取り回し AppleのサンプルからMPCSession関連の取り回しの実装を引用。 /* See LICENSE folder for this sample’s licensing information. Abstract: A class that manages peer discovery-token exchange over the local network by using MultipeerConnectivity. */ import Foundation import MultipeerConnectivity private struct MPCSessionConstants { static let kKeyIdentity: String = "identity" } class MPCSession: NSObject, MCSessionDelegate, MCNearbyServiceBrowserDelegate, MCNearbyServiceAdvertiserDelegate { var peerDataHandler: ((Data, MCPeerID) -> Void)? var peerConnectedHandler: ((MCPeerID) -> Void)? var peerDisconnectedHandler: ((MCPeerID) -> Void)? private let serviceString: String private let mcSession: MCSession private let localPeerID = MCPeerID(displayName: UIDevice.current.name) private let mcAdvertiser: MCNearbyServiceAdvertiser private let mcBrowser: MCNearbyServiceBrowser private let identityString: String private let maxNumPeers: Int init(service: String, identity: String, maxPeers: Int) { serviceString = service identityString = identity mcSession = MCSession(peer: localPeerID, securityIdentity: nil, encryptionPreference: .required) mcAdvertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: [MPCSessionConstants.kKeyIdentity: identityString], serviceType: serviceString) mcBrowser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: serviceString) maxNumPeers = maxPeers super.init() mcSession.delegate = self mcAdvertiser.delegate = self mcBrowser.delegate = self } // MARK: - `MPCSession` public methods. func start() { mcAdvertiser.startAdvertisingPeer() mcBrowser.startBrowsingForPeers() } func suspend() { mcAdvertiser.stopAdvertisingPeer() mcBrowser.stopBrowsingForPeers() } func invalidate() { suspend() mcSession.disconnect() } func sendDataToAllPeers(data: Data) { sendData(data: data, peers: mcSession.connectedPeers, mode: .reliable) } func sendData(data: Data, peers: [MCPeerID], mode: MCSessionSendDataMode) { do { try mcSession.send(data, toPeers: peers, with: mode) } catch let error { NSLog("Error sending data: \(error)") } } // MARK: - `MPCSession` private methods. private func peerConnected(peerID: MCPeerID) { if let handler = peerConnectedHandler { DispatchQueue.main.async { handler(peerID) } } if mcSession.connectedPeers.count == maxNumPeers { self.suspend() } } private func peerDisconnected(peerID: MCPeerID) { if let handler = peerDisconnectedHandler { DispatchQueue.main.async { handler(peerID) } } if mcSession.connectedPeers.count < maxNumPeers { self.start() } } // MARK: - `MCSessionDelegate`. internal func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { switch state { case .connected: peerConnected(peerID: peerID) case .notConnected: peerDisconnected(peerID: peerID) case .connecting: break @unknown default: fatalError("Unhandled MCSessionState") } } internal func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { if let handler = peerDataHandler { DispatchQueue.main.async { handler(data, peerID) } } } internal func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { // The sample app intentional omits this implementation. } internal func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { // The sample app intentional omits this implementation. } internal func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { // The sample app intentional omits this implementation. } // MARK: - `MCNearbyServiceBrowserDelegate`. internal func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) { guard let identityValue = info?[MPCSessionConstants.kKeyIdentity] else { return } if identityValue == identityString && mcSession.connectedPeers.count < maxNumPeers { browser.invitePeer(peerID, to: mcSession, withContext: nil, timeout: 10) } } internal func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { // The sample app intentional omits this implementation. } // MARK: - `MCNearbyServiceAdvertiserDelegate`. internal func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) { // Accept the invitation only if the number of peers is less than the maximum. if self.mcSession.connectedPeers.count < maxNumPeers { invitationHandler(true, mcSession) } } } MPCSessionの開始とNIDiscoveryToken交換後のNISession開始 MultipeerConnectivityによりNearby Interaction用のトークンを交換します。 // MARK: - Discovery token sharing and receiving using MPC. func startupMPC() { if mpc == nil { mpc = MPCSession(service: "ar100days", identity: "com.example.nearbyinteraction", maxPeers: 1) mpc?.peerConnectedHandler = connectedToPeer mpc?.peerDataHandler = dataReceivedHandler mpc?.peerDisconnectedHandler = disconnectedFromPeer } mpc?.invalidate() mpc?.start() } func dataReceivedHandler(data: Data, peer: MCPeerID) { guard let discoveryToken = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NIDiscoveryToken.self, from: data) else { fatalError("Unexpectedly failed to decode discovery token.") } peerDidShareDiscoveryToken(peer: peer, token: discoveryToken) } func shareMyDiscoveryToken(token: NIDiscoveryToken) { guard let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { fatalError("Unexpectedly failed to encode discovery token.") } mpc?.sendDataToAllPeers(data: encodedData) sharedTokenWithPeer = true } func peerDidShareDiscoveryToken(peer: MCPeerID, token: NIDiscoveryToken) { if connectedPeer != peer { fatalError("Received token from unexpected peer.") } peerDiscoveryToken = token let config = NINearbyPeerConfiguration(peerToken: token) session?.run(config) } NISessionDelegate関連処理 Nearby Interactionのdelegate処理。didUpdateで表示更新を行う。 // MARK: - NISessionDelegate func session(_ session: NISession, didUpdate nearbyObjects: [NINearbyObject]) { guard let peerToken = peerDiscoveryToken else { fatalError("don't have peer token") } let peerObj = nearbyObjects.first { (obj) -> Bool in return obj.discoveryToken == peerToken } guard let nearbyObjectUpdate = peerObj else { return } updateVisualization(peer: nearbyObjectUpdate) } func dataReceivedHandler(data: Data, peer: MCPeerID) { guard let discoveryToken = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NIDiscoveryToken.self, from: data) else { fatalError("Unexpectedly failed to decode discovery token.") } peerDidShareDiscoveryToken(peer: peer, token: discoveryToken) } func shareMyDiscoveryToken(token: NIDiscoveryToken) { guard let encodedData = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { fatalError("Unexpectedly failed to encode discovery token.") } mpc?.sendDataToAllPeers(data: encodedData) sharedTokenWithPeer = true } func peerDidShareDiscoveryToken(peer: MCPeerID, token: NIDiscoveryToken) { if connectedPeer != peer { fatalError("Received token from unexpected peer.") } peerDiscoveryToken = token let config = NINearbyPeerConfiguration(peerToken: token) session?.run(config) } 1m毎に空間にノードとテキストを配置 Dictionaryのキーとして距離を持ち、まだ配置していない場合にSCNNodeを配置。 距離の数値も表示したくなったためSCNTextも配置。表示が滑らかになる調整もしています。 private func addNode(distance: Float) { guard let camera = sceneView.pointOfView else { return } let node = SCNNode() node.geometry = SCNPlane(width: 0.6, height: 0.3) let material = SCNMaterial() material.isDoubleSided = true material.diffuse.contents = UIColor(red: 0.5, green: 0.5, blue: 0.9, alpha: 0.5) node.geometry?.materials = [material] let position = SCNVector3(x: 0, y: 0, z: 0) node.position = camera.convertPosition(position, to: nil) node.eulerAngles = camera.eulerAngles sceneView.scene.rootNode.addChildNode(node) let textGeometry = SCNText(string: String(format: "%0.2f m", distance), extrusionDepth: 0.8) textGeometry.font = UIFont(name: "HiraginoSans-W6", size: 100) textGeometry.firstMaterial?.diffuse.contents = UIColor.white let textNode = SCNNode(geometry: textGeometry) let (min, max) = (textNode.boundingBox) let w = Float(max.x - min.x) let h = Float(max.y - min.y) textNode.pivot = SCNMatrix4MakeTranslation(w/2 + min.x, h/2 + min.y, 0) textNode.position = camera.convertPosition(position, to: nil) textNode.eulerAngles = camera.eulerAngles textNode.scale = SCNVector3(0.0005, 0.0005, 0.0005) sceneView.scene.rootNode.addChildNode(textNode) } private func updateVisualization(peer: NINearbyObject) { guard let distance = peer.distance else { return } let _distance = Int(distance) if _distance > 0 && !placedNodeMeters.contains(_distance) { placedNodeMeters.append(_distance) addNode(distance: Float(_distance)) } UIView.animate(withDuration: 0.3, animations: { self.animate(peer: peer) }) } さいごに iPhoneの端末同士が背を向けている状態が一番精度が高まります。間に人がいるなどで遮蔽物がある場合や、端末の背面が向きあっていない場合に精度が落ちます。9mまで、と思い込んでいましたがそれ以上の距離でも高精度で計測できていました。とは言え離れるほど不安定になるとは思います。手元のテストでは直線で36mまで計測できました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift Concurrency で遅延実行

Swift Concurrency な環境で DispatchQueue を書きたくないので コード Before DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { print("遅延処理") } After Task { try await Task.sleep(nanoseconds: 1_000_000_000) print("遅延処理") } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】UserNotification

簡潔に分かりやすくまとめられているサイトを見つけたので自分のためにメモ https://dev.classmethod.jp/articles/ios-local-notification/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIKitでたまに遭遇するけど忘れがちなTipsメモ

UIKitで、たまに遭遇するけど忘れがちな部分をメモっておきます。 ViewやCell等でControlイベントが取れない場合のチェックリスト イベントを取りたいViewの isInteractionEnabled がtrueになってるか? イベントを取りたいViewの alpha が0になっていないか? backgroundColor = .clearにして、UIView.alphaは0より大きく設定する イベントを取りたいViewの上に、透明系のViewが被さってて、それがイベントを奪っていないか? storyboardでUIViewを作成すると、デフォルトで isInteractionEnabled = true になっているので、注意! storyboardでなくても、コードで追加したUIViewも同様なので、注意! イベントを透過させたいUIViewは、isInteractionEnabledをfalseにする そもそもoverride func hitTest()は取れてるか? pointInside() 参考: https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events UICollectionViewのselection管理 UICollectionViewやUITableViewには、セルの選択状態を管理する機能が備わっています。 isSelectedは、collectionView側で管理されている スクロールでCellが見えなくなり、(Cellが再利用され)、スクロールで再度表示した際にはcell.isSelectedが復元されている collectionView.indexPathsForSelectedItems()で、選択状態のindexPathの配列が取れる Cellは画面外にスクロールすると再利用されるのに、選択状態は見えない範囲も全て管理してるのが、意外といえば意外でした。 参考 https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/changing_the_appearance_of_selected_and_highlighted_cells https://developer.apple.com/documentation/uikit/uitableviewdelegate/selecting_multiple_items_with_a_two-finger_pan_gesture UITextFieldのshouldChangeCharactersInをRangeで扱う NSRangeはNSStringに対応している。(UTF-16) RangeはStringに対応する。(Unicode Scalar + 正規化) 無理に使うと、絵文字を入れた時等にクラッシュする事がある。(特に絵文字バリエーションシーケンスの扱いが違う) 参考 https://qiita.com/doraTeX/items/4feea8d9221419b19954 https://moji-memo.hatenablog.jp/entry/20120802/1343876603 (なぜここだけNSRangeのまま残ってしまってるのか…) public func textField(_ textField: UITextField, shouldChangeCharactersIn nsRange: NSRange, replacementString string: String) -> Bool { guard let text = textField.text, let range = Range(nsRange, in: text) else { return false } /* ... */ } NavigationBarをスクロールの方向で隠す 比較的簡単でTwitterっぽい自然な感じの動きにしてみました。 バウンスに反応しないようisTrackingを見ています。 class MyViewController { @IBOutlet private var collectionView: UICollectionView! private var lastContentOffset: CGPoint = .zero private var lastTracking = false override func viewDidLoad() { super.viewDidLoad() collectionView.delegate = self } // 画面遷移前には表示に戻しておく override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.setNavigationBarHidden(false, animated: true) } } extension MyViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let dy = scrollView.contentOffset.y - lastContentOffset.y lastContentOffset = scrollView.contentOffset let didTouchUp = lastTracking && !scrollView.isTracking lastTracking = scrollView.isTracking // 下にスクロールしたら隠す if scrollView.isTracking && dy > 5 { navigationController?.setNavigationBarHidden(true, animated: true) } // 指を離したタイミングで上にスクロールしていたら表示 if didTouchUp && dy < -10 { navigationController?.setNavigationBarHidden(false, animated: true) } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【キュンです】指ハートを機械学習してエフェクトを出す

Macで機械学習でHand Pose Classification(手のポーズ分類)ができます。 今回はデモとして、CreateMLでポーズ認識モデルを作成して、「指ハート」と「ピースサイン」を認識するスマホアプリを作ってみます。 【キュンです】 動画解説:Youtube 1、データを集める 学習には画像データが必要です。 集めてフォルダ分けします。 必要な画像: ・識別したいポーズの画像(今回は"fingerHeart"と"peace") ・識別する必要のないポーズの画像("background") を、それぞれのクラスごとにフォルダ分けします。 "background"には、識別する必要のないさまざまなポーズと、分類したいポーズに移行するときの中途半端な手の画像を含めます。 さまざまな肌の色、年齢、性別、照明条件の画像を用意します。 今回はそれぞれのクラスで200枚前後を用意しました。 2、CreateMLでモデルをトレーニングする CreateMLをひらきます。 (Xcodeをコントロールクリックして、OpenDeveloperToolsから開く。) HandPoseClassificationを選択します。 (HandPoseClassificationは、macOS Monterey / Xcode13以上の環境で使えます。) データセット(Training Data)を選択。 検証データは指定しなければ、自動で生成されます。 データセット拡張(Augmentations:画像を回転させたりして水増し)を適宜設定します。今回は手の左右は問わないので、水平反転と回転を入れてみました。 Trainを押して学習を開始します。 数分で学習が終わります。 トレーニングが終わると、Previewタブから手持ちの画像でテストができます。 macのカメラでライブプレビューもできます。 Outputタブからモデルを入手します。 3、アプリでモデルを使用する モデルをXcodeプロジェクトにドロップして初期化します。 import CoreML import Vision ... let model = try? MyHandPoseClassifier_1(configuration: MLModelConfiguration()) Visionフレームワークで手のポイント(指先、関節などのキーポイントの位置:上記CreateMLプレビューの点の位置)を検出してからモデルに入力します。 func session(_ session: ARSession, didUpdate frame: ARFrame) { // 今回はARSessionからカメラフレームを取得します let pixelBuffer = frame.capturedImage // 手のポーズの検出リクエストを作成 let handPoseRequest = VNDetectHumanHandPoseRequest() // 取得する手の数 handPoseRequest.maximumHandCount = 1 // カメラフレームで検出リクエストを実行 // カメラから取得したフレームは90度回転していて、 // そのまま推論にかけるとポーズを正しく認識しなかったりするので、 // orientationを確認する let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .right, options: [:]) do { try handler.perform([handPoseRequest]) } catch { assertionFailure("HandPoseRequest failed: \(error)") } guard let handPoses = handPoseRequest.results, !handPoses.isEmpty else { return } // 取得した手のデータ guard let observation = handPoses.first else { return } // 毎フレーム、モデルの推論を実行すると処理が重くなり、 // ARのレンダリングをブロックする可能性があるので、インターバルをあけて推論実行する frameCounter += 1 if frameCounter % handPosePredictionInterval == 0 { makePrediction(handPoseObservation: observation) frameCounter = 0 } } (注)カメラから取得したフレームは90度回転していて、そのまま推論にかけるとポーズを正しく認識しなかったりします。フレームの向き(orientation)を確認してから後続の作業をした方がいいと思います 取得した手のポイントのデータをMultiArray(多次元配列)に変換しCoreMLでモデルに入力・推論実行します。 func makePrediction(handPoseObservation: VNHumanHandPoseObservation) { // 手のポイントの検出結果を多次元配列に変換 guard let keypointsMultiArray = try? handPoseObservation.keypointsMultiArray() else { fatalError() } do { // モデルに入力して推論実行 let prediction = try model!.prediction(poses: keypointsMultiArray) let label = prediction.label // 最も信頼度の高いラベル guard let confidence = prediction.labelProbabilities[label] else { return } // labelの信頼度 print("label:\(prediction.label)\nconfidence:\(confidence)") } catch { print("Prediction error") } } label:fingerHeart confidence:0.9999963045120239 4、手のポーズに応じてARをつける 取得した分類ラベルに応じて処理をスイッチします。 if confidence > 0.9 { // 信頼度が90%以上で実行 switch label { case "fingerHeart":displayFingerHeartEffect() case "peace":displayPeaceEffect() default : break } } エフェクトを指の位置に出現させるために、 Visionで取得した指の位置をカメラから20cm奥にマッピングします。 func getHandPosition(handPoseObservation: VNHumanHandPoseObservation) -> SCNVector3? { // 人差し指の第二関節の位置に出現させる guard let indexFingerPip = try? handPoseObservation.recognizedPoints(.all)[.indexPIP], indexFingerTip.confidence > 0.3 else {return nil} // Visionの指の位置の検出結果は0~1に正規化されているので、 // view.boundsのサイズに直す。これにはVisionの関数が使える。 // また、Visionの座標原点は左下なので、Yを反転させてviewの座標システムに合わせる let deNormalizedIndexPoint = VNImagePointForNormalizedPoint(CGPoint(x: indexFingerTip.location.x, y:1-indexFingerTip.location.y), viewWidth, viewHeight) // 指はカメラから20cm奥にあると想定する let infrontOfCamera = SCNVector3(x: 0, y: 0, z: -0.2) guard let cameraNode = arScnView.pointOfView else { return nil} // カメラのワールド座標位置から20cm奥の位置を求める let pointInWorld = cameraNode.convertPosition(infrontOfCamera, to: nil) // view平面内の2次元の人差し指の位置と上記20cm奥の位置をスクリーン平面に定義する var screenPos = arScnView.projectPoint(pointInWorld) screenPos.x = Float(deNormalizedIndexPoint.x) screenPos.y = Float(deNormalizedIndexPoint.y) // スクリーン平面の指の位置を3次元座標にマッピングする let finalPosition = arScnView.unprojectPoint(screenPos) return finalPosition } SceneKitをつかって、3Dオブジェクトをアニメーションさせます。 func displayFingerHeartEffect(){ guard !isEffectAppearing else { return } // エフェクトが起動中かチェック isEffectAppearing = true // 人差し指の第二関節の位置を取得 guard let handPoseObservation = currentHandPoseObservation,let indexFingerPosition = getHandPosition(handPoseObservation: handPoseObservation) else {return} // エフェクトを指の位置に移動 heartNode.position = indexFingerPosition // アニメーションを定義 let fadeIn = SCNAction.fadeIn(duration: 0.2) let up = SCNAction.move(by: SCNVector3(x: 0, y: 0.1, z: 0), duration: 0.1) let shakeHalfRight = SCNAction.rotate(by: -0.3, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.025) let shakeLeft = SCNAction.rotate(by: 0.6, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.05) let shakeRight = SCNAction.rotate(by: -0.6, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.05) let shakeHalfLeft = SCNAction.rotate(by: 0.3, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.025) let shake = SCNAction.sequence([shakeLeft,shakeRight]) let fadeOut = SCNAction.fadeOut(duration: 1) let shakeRepeat = SCNAction.sequence([shakeHalfRight,shake,shake,shake,shake,shakeHalfLeft]) let switchEffectAppearing = SCNAction.run { node in // エフェクトのフラグをoffにしておく self.isEffectAppearing = false } // アニメーションを実行 heartNode.runAction(.sequence([fadeIn,up,shakeRepeat,fadeOut,switchEffectAppearing])) } 【キュンです】 このデモのGitHubリポジトリ(Xcodeプロジェクト): HandPoseClassificationAR 引用/参考文献: 手のポイントの取得について: VisionでHand Pose Detection 手のトラッキング: MLBoyだいすけ 2次元座標の3次元へのマッピングはKBoy様のコードを使わせていただきました: ARKitのための3D数学: Kei Fujikawa ? フリーランスエンジニアです。 お仕事のご相談こちらまでお気軽に? rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 GitHub Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

動画からスライドを自動生成するアプリを作りました

この記事は クソアプリアドベントカレンダー2 の2日目の記事です。今まで傍から見ているだけでしたがついに初参加できて嬉しいです。 課題: 動画よりもスライドでサクサク見たい WWDCセッションの内容をチェックしては発信する、ということを毎年やっている 1 のですが、なぜか昨年(2020)あたりからセッションのスライドPDFが公開されなくなりました。 セッションの動画は例年通り公開されているのですが、 動画よりもスライドのほうがシーク効率が圧倒的に良いので、やっぱりスライドが欲しい。2 で、つくったのが、 動画からスライドを抽出するiOS/macOSアプリ「Slidify」 ↑この動画をSlidifyに食わせて全自動で生成したスライドが、 ↑こちら。 いかがでしょう? 荒削りな部分(後述)はあるものの、私は自分用ツールとしては十分実用に耐えうると思いました。 しくみ スライド抽出のしくみはざっくり以下のとおりです: 動画のフレームを一定間隔ごとに解析 以下の条件を満たすフレームだけを抽出 文字がある 文字領域が前フレームから一定以上変化している これだけ。 上のロジックに該当する実装を抜粋すると、こんな感じです: 一定間隔ごとの動画解析 let processingOption = VNVideoProcessor.RequestProcessingOptions() processingOption.cadence = VNVideoProcessor.TimeIntervalCadence(processingTimeInterval) let videoProcessor = VNVideoProcessor(url: url) // request は VNDetectTextRectanglesRequest try! videoProcessor.addRequest(request, processingOptions: processingOption) let timeRange = CMTimeRange(start: CMTime.zero, end: CMTime.indefinite) try! videoProcessor.analyze(timeRange) 文字があるか? // observations は VNTextObservation の配列 guard observations.count > 0 else { return false } 文字領域が前フレームから一定以上変化しているか? let intersection = bbox1.intersection(bbox2) if intersection.area / bbox1.area < threshold { return true } 文字認識も、抽出したフレーム画像同士の類似度判定も行っていません。 こういう簡素な抽出ロジックのため、 メガネに映り込んだアイコン画像の中の文字を拾ってしまっていたり 同じようなページが連続してしまっていたり といった問題も散見されますが、 「スライドをざーっと見て概要を把握したい」という自分の用途としては重大な問題ではありませんでした。 実装はほぼ使いまわし ほとんどの実装は、過去の個人開発アプリからの流用です。 動画選択UI Chopper からの流用 PHPickerViewControllerとUIDocumentPickerViewControllerを利用 文字領域検出 Somato からの流用 文字領域検出だけでなく、Visionラッパーとして自分が使いやすいようにモジュール化してある TOTOCでも同パッケージを使っている 動画からのフレーム抽出 Somato, TOTOCからの流用 今回Slidify用に独自実装した部分は文字領域の差分判定のところぐらい。 使い回しやすいパーツはSwift Package化しておいて、別プロジェクトにサクッと横展開できるようにしてあります。 今後の展望 以下対応しておくともっと便利に使えそう: 類似画像の除去 コマンドラインツール化 コンテキストメニュー対応 動画ファイル選択 → 右クリックで実行できるようにしたい おわりに 作り終えて記事を書き始めてから思いましたが、クソアプリカレンダーには真面目すぎる題材だったかもしれません... 「作りたい!という衝動にまかせて、有用性や需要のことは考えずとにかく手を動かす」 というものづくりの姿勢から生まれたものをクソアプリと呼ぶ、と拡大解釈して投稿させていただきました。もし場違いでしたらすみません??‍♂️こういうネタ要素の少ないものでもよろしければぜひまた参加させてください。 OSSを作ったり、本を書いたり、noteマガジンを書いたり。 ↩ しっかり調査する際には結局動画を見るのだとしても、どの動画を見るかどうか決めるにあたって、まずはスライドをパラパラと見てざっと概要を把握したい。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリをApp Storeに公開するまでの流れ

はじめに こんにちは、HCB Advent Calendar 2021の3日目を担当する、kudokaiです。 今年は個人開発のアプリをApp Storeにリリースしたこともあり、ストアに申請を出す手順を備忘録的にまとめようと思います。 リリースしてから4ヶ月ほど経つので思い出す作業も兼ねて書きます。 ここではApple Developerへの登録手順の説明は省きます。 ※記事内に添付しているスクリーンショットは2021/12/2時点のものです。 申請までの大まかな流れ Certificate作成 App IDs登録 Provisioning Profile作成 アプリをApp Store Connectに登録 Xcodeを用いてアプリをアップロード App Store Connectでアプリの情報を編集 スクリーンショット アプリアイコン プロモーション用テキスト 概要 キーワード サポートURL ビルド情報 申請 やること多いんですよね... Certificate作成 ここからはApple Developerサイトにアクセスして作業するのですが、公式で使用するブラウザはSafariを推奨しています。 Apple DeveloperサイトのCertificates Identifiers & Profiles にアクセス Certificatesを選択し、+ボタンをクリック iOS Distribution (App Store and Ad Hoc)をクリック キーチェーンを使って作成したファイルを追加 App IDs登録 Identifiersを選択し、+ボタンをクリック App IDsをクリック 次の画面で「App」を選択し、Continueをクリック DescriptionにAppIDの説明、Bundle IDに開発中のアプリのBundle identifierを入力し、Explicitを選択 Provisioning Profile作成 僕はこの辺りがよく分からずにAppleに何度も問い合わせメール送り、Xcode上でAutomatically manage signingにチェックを入れるとProvisioning Profileを自動で管理してくれるので特別な操作は不要になることが分かりました。 Automatically manage signing便利ですね。 Automatically manage signingにチェックを入れていない場合の流れを以下に記載します。 Profilesを選択し、+ボタンをクリック App Storeを選択し、Continueをクリック 「App ID:」の欄から先ほど作成したApp IDを選択し、Continueをクリック 該当するCertificateを選択し、Continueをクリック Provisioning Profile Nameに任意の名前を入力し、Ganereteをクリック アプリをApp Store Connectに登録 Apple DeveloperサイトのApp Store Connect にアクセスし、マイAppをクリック。 次の画面で+ボタンをクリックし、「新規App」を選択 ポップアップが表示されたら以下を入力 プラットフォーム: iOS 名前: アプリ名 プライマリ言語: App情報がローカライズされていない国または地域で使用する言語 バンドルID: 該当するバンドルID SKU: Appに割り当てる固有ID Xcodeを用いてアプリをアップロード Xcodeを立ち上げ、Products→Archiveを選択。 Archiveが完了するまでしばらく待つ。 Validate Appをクリック その後、画面の指示通り進んでいき、Validateまでクリック Distibute Appをクリック その後、App Store Connect→Uploadの順に選択。 Upload可能になったら「Upload」ボタンをクリック。 App Store Connectでアプリの情報を編集 ここではリリース申請に必要なアプリ情報を編集する。 Apple DeveloperサイトのApp Store Connect にアクセスする。 申請には6.5インチ・5.5インチ・12.9インチのスクリーンショットが必要になります。 それぞれ最低1枚ずつが必要です。 プロモーション用テキスト: アプリの説明欄の上部に表示される文章 概要: アプリの概要、機能の説明やアプリの特徴など キーワード: App Storeで検索する際のキーワード サポートURL: 問い合わせ用のURL マーケティングURL: 別に空欄でも可 Xcodeからアップロードしたビルドバージョンを選択する 右側のメニューから「App情報」を選択する。 ここではアプリの名前・サブタイトル・カテゴリを編集する。 名前: アプリのタイトル サブタイトル: 検索時にアプリタイトルの下に表示される カテゴリ: アプリが分類されるカテゴリをプルダウンから選択 プライバシーポリシーのURLを記載する。 入力し終えたら保存ボタンをクリック。 申請 必要なデータを全て入力したら「審査へ提出」ボタンをクリックします。 提出し終えたら確認メールが届くので後は、申請が通ることを祈って待ちます。 無事、申請が通るとAppleからメールが届くので、App Storeに開発したアプリが公開されているか確認しましょう。 大体が48時間以内に申請結果が返ってくるそうです。 僕の場合は10時間くらいで申請結果が返ってきて、アプリを公開することができました。 さいごに ここまでとても長かったですね... 僕自身、人生初めてのリリース作業だったのでとても時間がかかりました。 これからiOSアプリのリリース申請をする人へのお役に立てば幸いです。 内容の不備などがあればお知らせいただければ嬉しいです。 次のHCB Advent Calendarは、また僕が書く予定なので、お楽しみに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む