GPUImageでカメラの画像にリアルタイムでフィルターをかける
GPUImageというライブラリを使って、カメラ映像にリアルタイムでフィルターをかける事を試してみました。
まずはCocoaPodsでGPUImageをインストールします。
target 'MyApp' do use_frameworks! pod 'GPUImage' end
次はInfo.plistのNSCameraUsageDescriptionキーに、カメラ使用理由を追加します。
続けて画面にカメラの映像を表示してみます。
適当なViewControllerを以下のように修正します。
import UIKit import GPUImage class ViewController: UIViewController { let camera = GPUImageVideoCamera(sessionPreset: AVCaptureSession.Preset.vga640x480.rawValue, cameraPosition: .back) override func viewDidLoad() { super.viewDidLoad() camera?.outputImageOrientation = .portrait let imageView = GPUImageView(frame: view.bounds) view.addSubview(imageView) camera?.addTarget(imageView) camera?.startCapture() } }
カメラの映像を撮っているのはGPUImageVideoCameraというクラスです。
このクラスは画面サイズやどのカメラを使うかなどの情報を渡して初期化します。
let camera = GPUImageVideoCamera(sessionPreset: AVCaptureSession.Preset.vga640x480.rawValue, cameraPosition: .back)
カメラの映像を画面に表示しているのが下の処理です。
GPUImageViewというビューを画面に貼り付け、それをカメラに紐付ける事で、カメラ映像を画面に表示しています。
let imageView = GPUImageView(frame: view.bounds) view.addSubview(imageView) camera?.addTarget(imageView)
続けて映像にリアルタイムフィルターを適用してみます。
先程の処理を下のように置き換えます。
class ViewController: UIViewController { let camera = GPUImageVideoCamera(sessionPreset: AVCaptureSession.Preset.vga640x480.rawValue, cameraPosition: .back) override func viewDidLoad() { super.viewDidLoad() camera?.outputImageOrientation = .portrait let imageView = GPUImageView(frame: view.bounds) view.addSubview(imageView) let filter = GPUImageiOSBlurFilter() camera?.addTarget(filter) filter.addTarget(imageView) camera?.startCapture() } }
今回の修正箇所は下の部分です。
フィルタークラス(GPUImageiOSBlurFilter)を生成して、それをカメラとImageViewと紐付けています。
let filter = GPUImageiOSBlurFilter() camera?.addTarget(filter) filter.addTarget(imageView)
下が実行時の映像です。
分かりにくいのですが、カメラ映像にぼかし処理がかかっています。
AVAudioEngineでmp3をエフェクト付きで再生する
AVAudioEngineというクラスを使って、mp3をエフェクト付きで再生してみました。
まずはプロジェクトにmp3ファイルを追加します。
追加したファイルがBuild Phasesにも入っているか確認します。
入っていない場合は+ボタンからmp3を追加します。
まずは普通にmp3を再生します。
下のようにAVAudioEngineとAVAudioPlayerNodeのインスタンスを作成、それらをconnectする事で再生します。
import UIKit import AVFoundation class ViewController: UIViewController { let engine = AVAudioEngine() override func viewDidLoad() { super.viewDidLoad() let player = AVAudioPlayerNode() if let path = Bundle.main.path(forResource: "music", ofType: "mp3") { let url = URL(fileURLWithPath: path) if let file = try? AVAudioFile(forReading: url) { engine.attach(player) engine.connect(player, to: engine.mainMixerNode, format: file.processingFormat) player.scheduleFile(file, at: nil, completionHandler: nil) try? engine.start() player.play() } } } }
もしmp3を再生するだけならAVAudioPlayerを使う方が簡単そうでした。
続けてエフェクトをかけてみます。
ViewControllerを以下のように書き換えます。
下コードでは再生速度を2倍にしています。
class ViewController: UIViewController { let engine = AVAudioEngine() override func viewDidLoad() { super.viewDidLoad() let player = AVAudioPlayerNode() if let path = Bundle.main.path(forResource: "music", ofType: "mp3") { let url = URL(fileURLWithPath: path) if let file = try? AVAudioFile(forReading: url) { engine.attach(player) let unit = AVAudioUnitVarispeed() unit.rate = 2.0 engine.attach(unit) engine.connect(player, to: unit, format: file.processingFormat) engine.connect(unit, to: engine.mainMixerNode, format: file.processingFormat) player.scheduleFile(file, at: nil, completionHandler: nil) try? engine.start() player.play() } } } }
今回の変更点は下の箇所です。
AVAudioUnitVarispeedという再生スピード変更ノードを作成してそれをconnectしています。
engine.attach(player) let unit = AVAudioUnitVarispeed() unit.rate = 2.0 engine.attach(unit) engine.connect(player, to: unit, format: file.processingFormat) engine.connect(unit, to: engine.mainMixerNode, format: file.processingFormat)
続けて違うエフェクトも使ってみます。
次はAVAudioUnitTimePitchを試しました。
AVAudioUnitTimePitchは音程と速度を調整するユニットで、音の高低を変更したり音程そのままで速度アップしたりできます。
let unit = AVAudioUnitTimePitch() unit.pitch = 1200.0 engine.attach(unit) engine.connect(player, to: unit, format: file.processingFormat) engine.connect(unit, to: engine.mainMixerNode, format: file.processingFormat)
ファイルへの書き込みは下のように行います。
installTapメソッドでbufferを取得できるので、それを順番にファイルに書き込んでいきます。
if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let settings = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100.0, AVNumberOfChannelsKey: 2 ] as [String : Any] if let file = try? AVAudioFile(forWriting: url.appendingPathComponent("test.aac"), settings: settings) { unit.installTap(onBus: 0, bufferSize: 4096, format: unit.inputFormat(forBus: 0), block: { buffer, when in try? file.write(from: buffer) }) } }
録音を停止したい場合はremoveTapメソッドを利用します。
unit.removeTap(onBus: 0)
Phones.frameworkで端末の写真の取得・保存・削除
Phones.frameworkを使って、端末の写真の取得・保存・削除を試してみました。
写真を操作する前にInfo.plistにNSPhotoLibraryUsageDescriptionに写真を操作する理由を記述します。
これがないとアプリがクラッシュします。
取得
端末写真をUIImageとして取得する処理は下の通りです。
PHAsset.fetchAssets
でPHAssetを取得、PHImageManagerと取得したPHAssetを使ってUIImageを読み込んでいます。
import UIKit import Photos class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let options = PHFetchOptions() let result = PHAsset.fetchAssets(with: .image, options: options) if let asset = result.firstObject { PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .aspectFill, options: nil, resultHandler: { image, options in let imageView = UIImageView(image: image) imageView.frame.origin.x = (self.view.frame.width - imageView.frame.width) / 2 self.view.addSubview(imageView) }) } } }
起動すると写真が表示されていることが分かります。
fetchAssetsの戻り値は下のようなメソッドを持っています。
let result = PHAsset.fetchAssets(with: .image, options: PHFetchOptions()) result.count // → 件数 result.object(at: 0) // → 0番目の要素(PHAsset) result.objects(at: [0, 1, 2]) // → 0, 1, 2番目の要素の配列 result.countOfAssets(with: .audio) // → audioアセットの数を取得
取得の際はソート・NSPredicateを使った絞り込み・取得件数の指定も可能です。
let options = PHFetchOptions() options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ] options.predicate = NSPredicate(format: "pixelWidth < 2000") options.fetchLimit = 1 let result = PHAsset.fetchAssets(with: .image, options: options)
削除
削除処理は下の通りです。
PHAssetChangeRequestのdeleteAssetsメソッドにPHAssetを渡す事で削除します。
if !asset.canPerform(.delete) { return } PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.deleteAssets(NSArray(array: [asset])) }, completionHandler: { _, _ in // 削除後の処理 })
削除の前に、自動で下のダイアログが表示されます。
PHAssetChangeRequestのdeleteAssetsですが、performChangesメソッドを使わずに呼び出すと下のようなエラーになります。
// 単独で呼ぶと下のようなエラー PHAssetChangeRequest.deleteAssets(NSArray(array: [asset])) // 'NSInternalInconsistencyException', reason: 'This method can only be called from inside of -[PHPhotoLibrary performChanges:completionHandler:] or -[PHPhotoLibrary performChangesAndWait:error:]'
保存
保存は下の通りです。
PHAssetChangeRequestのcreationRequestForAssetにUIImageを渡す事で端末への保存ができます。
let image = UIImage() PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAsset(from: image) }, completionHandler: { _, _ in // 作成後の処理 })
【iOS】ユーザーがスクリーンショットを撮ったタイミングを検知する
スクリーンショットを撮ったタイミングを検知するには.UIApplicationUserDidTakeScreenshotを使います。
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(self.userDidTakeScreenshot), name: .UIApplicationUserDidTakeScreenshot, object: nil) } @objc func userDidTakeScreenshot() { print(#function) } }
スクリーンショットを撮ったら登録したメソッドが呼ばれました。
アプリがバックグラウンドにある時も試したのですが、バックグラウンド時は登録したメソッドは呼ばれないようでした。
モバイル版GoogleAnalyticsでFirebaseを選ばない方法
最近はGoogleAnalyticsプロパティー作成時にモバイルアプリを選ぶとFirebase一択になりました。
今日はモバイルアプリでもFirebaseなしのGoogleAnalyticsを使う方法を書いてみます。
まずはウェブサイトとしてプロパティーを作成します。
続けてそのプロパティーを選択してビュー追加画面に移動します。
そこではFirebaseを使わないモバイルアプリを選べるので、選択して作成します。
これでFirebaseなしのGoogleAnalyticsを使う事ができます。
参考URL
iOS11 & Xcode9 で UINavigationController の pushViewController が斜めになる問題対策
push遷移アニメーションが下のように斜めになってしまったので対策を調べてみました。
stackoverflowによると、遷移先のviewDidLoadでtableViewのcontentInsetAdjustmentBehaviorを.neverにすれば良いとのことでした。
popViewControllerの時のアニメーションは遷移元のcontentInsetAdjustmentBehaviorによるので、遷移先・遷移元の両方でセットすると良いかと思います。
if #available(iOS 11.0, *) { tableView.contentInsetAdjustmentBehavior = .never }
1つ1つのtableViewに設定するのが大変な時は、appearanceで設定すれば一括変更されます。
if #available(iOS 11.0, *) { UITableView.appearance().contentInsetAdjustmentBehavior = .never }
それとこちらの現象はUITableViewStyleがgroupedでもplainでも発生するようです。
Xcode9で新規作成したアプリではこの現象は発生しませんでした。
比較したところ、adjustedContentInsetというreadonlyプロパティーの値が違っていました。
今回は調べきれなかったのですが、adjustedContentInsetの値を変更すればcontentInsetAdjustmentBehaviorを使わずに対応できるかもしれません。
print(tableView.adjustedContentInset) // → Xcode9で作成したアプリはtopに64が入ってた、Xcode8以前で作成したアプリのtopは0だった
2017/10/13追記
下の方法ですが、UITableViewがタブバーやツールバーと被ってしまうという問題があるようです。
なので下方法を使う場合はUITableViewのtableFooterViewに何か置くなどの対処が必要になりそうです。
if #available(iOS 11.0, *) { UITableView.appearance().contentInsetAdjustmentBehavior = .never }
それと新規アプリがpushViewControllerで斜めにならないのはUINavigationBarのisTranslucentをtrueにしていたからでした。
この値をfalseにするとpushViewControllerが斜めになってしまうようです。
UINavigationBar.appearance().isTranslucent = false
参考URL
2017/10/30追記
TableViewがタブバーと被る問題、UITableViewControllerに下コードを足すことでも解消できました。
override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() if #available(iOS 11.0, *) { tableView.contentInset.bottom = tabBarController?.tabBar.frame.height ?? 0 } }
参考URL
iOS11 + Xcode9.0でedgesForExtendedLayoutの値を空にしていると、UITableViewのドリルダウンでアニメーションが崩れる
【Swift4】形態素解析をやってみる | NSLinguisticTagger
NSLinguisticTaggerというクラスで形態素解析ができるようなので試してみました。
import UIKit let text = "I have a beautiful pen." let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0) tagger.string = text tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .tokenType, options: []) { tag, tokenRange, sentenceRange, _ in let subString = (text as NSString).substring(with: tokenRange) print("\(subString) : \(tag?.rawValue ?? "")") }
文字列を渡した所、下のように分解してくれました。
WhiteSpaceは引数のオプションに.omitWhitespaceを渡すことで除外できます。
let text = "I have a pen." let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0) tagger.string = text tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .lexicalClass, options: [.omitWhitespace]) { tag, tokenRange, sentenceRange, _ in let subString = (text as NSString).substring(with: tokenRange) print("\(subString) : \(tag?.rawValue ?? "")") }
日本語の分解も可能です。
let text = "関東機械学習協会員" let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0) tagger.string = text tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .tokenType, options: []) { tag, tokenRange, sentenceRange, _ in let subString = (text as NSString).substring(with: tokenRange) print("\(subString) : \(tag?.rawValue ?? "")") }
漢字が並んでいる場合やひらがなが並んでいる場合もうまく取得できました。
enumerateTagsの引数のscemeにlexicalClassを渡すと品詞分解してくれます。
let text = "I have a beautiful pen." let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0) tagger.string = text tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .lexicalClass, options: []) { tag, tokenRange, sentenceRange, _ in let subString = (text as NSString).substring(with: tokenRange) print("\(subString) : \(tag?.rawValue ?? "")") }
品詞分解は日本語非対応です。
言語がどのスキーマに対応しているかどうかはavailableTagSchemesを使うと確認できます。
print(NSLinguisticTagger.availableTagSchemes(forLanguage: "en").map { $0.rawValue }) print(NSLinguisticTagger.availableTagSchemes(forLanguage: "ja").map { $0.rawValue })
languageを渡すと言語を判定してくれます。
let text = "I have a beautiful pen." let tagger = NSLinguisticTagger(tagSchemes: NSLinguisticTagger.availableTagSchemes(forLanguage: "en"), options: 0) tagger.string = text tagger.enumerateTags(in: NSRange(location: 0, length: text.characters.count), scheme: .language, options: []) { tag, tokenRange, sentenceRange, _ in let subString = (text as NSString).substring(with: tokenRange) print("\(subString) : \(tag?.rawValue ?? "")") }
日本語、日本語と英語ミックスでも正しく判定できました。