2017年3月30日木曜日

Xcode8.3で変わったこと(気づいたことだけ)

macOSとiOSがアップデートされ、同時にXcodeも8.3に上がった。
そしたらまたいくらか仕様の変更があった模様。

気づいたことだけまとめる。

M_PIが非推奨に

πのM_PIが非推奨になり、Double.pi.piを使えってWarningが出た。
でも.piに置き換えるとまたエラーになるので、Double.piに置き換えた。

Swift2.3が非サポートに

Swift3.xだけのサポートになったみたい? コードのコンバート機能は残ってると思うのでそいつでどうぞ。あんまり便利じゃないけど。

2017年3月9日木曜日

モーションセンサー、うまくいくようないかないような



モーションセンサーには以下のようなものがある。
値の範囲は決まっているはずだが、センサーの精度の問題なのか、範囲の上限/下限値にならないことも多く、上限/下限値をまたいだ処理をさせると値が飛んでしまうこともあり困る。
プロはどうやってスムーズな処理をしてるんだろう? 誰かおせーて!!
  • 加速度センサー(Accelerometer)
    • X軸、Y軸、Z軸方向に動かした場合、どちらの方向にどれだけ加速度がかかったか
  • ジャイロセンサー(Gyro)
    • X軸、Y軸、Z軸を軸として傾けた場合の、1秒あたりに傾いたラジアン角を得られる(角速度)
  • 姿勢センサー(Attitude)
    • X軸、Y軸、Z軸を軸として傾けた場合の絶対角を得られる。水平な台の上に画面を上にして寝かせて置いた時がそれぞれ0度。
    • X軸中心の前後の傾きがpitch
      • 画面上で寝かせて0度。起こしていくと増えていき、垂直で90度。さらに画面下向きに寝かせていくと減っていき0度に。
      • 増えていった値が90度を境に減るはずだが、時には87度前後で減り出したり、よくわからない。
    • Y軸中心の左右の回転がroll
      • pitchが90度を超えると急に-150度とかになったり、わからない
    • Z軸中心の左右の傾きがyaw
      • これがわずかに傾いてるだけでもpitchの値が90までいかないうちに減り出すことがあるようだ。
  • 重力加速度(Gravity)
    • 地球の重力に対しての向き
    • X軸はLandscapeでホームボタンが右なら-1.0、左なら1.0、Portrateなら0
    • Y軸はPortrateでホームボタンが下なら-1.0、上なら1.0、Landscapeなら0
    • Z軸は画面が真上なら-1.0、垂直に立てると0、画面が真下なら1.0
ライブラリはあらかじめCoreMotionをimportしとく。
import CoreMotion //モーションセンサー

モーションマネージャーはインスタンス変数として作っておかないと、ARCですぐに消されちゃうので注意。
var motionMgr:CMMotionManager?

センサーの設定とスタートまで

//モーションマネージャーは各種センサー共通
motionMgr = CMMotionManager()
//更新頻度(秒)
motionMgr?.deviceMotionUpdateInterval = 0.1 //姿勢センサー
//姿勢センサースタート(クロージャでセンサーが更新された時の処理を書く)
motionMgr?.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler: { (data, error) in
        let pitchDeg = round(180 * data!.attitude.pitch / M_PI)
        let rollDeg = round(180 * data!.attitude.roll / M_PI)
        let yawDeg = round(180 * data!.attitude.yaw / M_PI)

})

//姿勢センサーの停止
motionMgr?.stopDeviceMotionUpdates()

ローパスフィルター

センサーの値がノイズによってばらけることが多いらしく、前回検出した値との加算平均を取る処理(これをローパスフィルターとかハイパスフィルターとか言うらしい)をした方がいいそうである。
最新の値の90%と、前回の値の10%を足すわけですな。


//ローパスフィルター処理(ここでは0.9がレート)
var outputValue:Double
if (self.prevAngle != nil) {
        outputValue = (pitchDeg * 0.9) + (self.prevAngle! * (1.0 - 0.9))
    } else {
        outputValue = pitchDeg
    }
self.prevAngle = outputValue

他のセンサー使用時

センサーの値を取り出す各種プロパティは省略。


//各センサーの更新頻度(秒)設定
motionMgr?.accelerometerUpdateInterval = 0.1 //加速度センサー
motionMgr?.gyroUpdateInterval = 0.1 //ジャイロセンサー

//各センサーのスタート(引数は省略したが姿勢センサーと同様に)

motionMgr?.startAccelerometerUpdates() //加速度センサー

motionMgr?.startGyroUpdates() //ジャイロセンサー



//各センサー停止

motionMgr?.stopAccelerometerUpdates() //加速度センサー
motionMgr?.stopGyroUpdates() //ジャイロセンサー

参考サイト

2017年3月8日水曜日

画像の合成、初歩の初歩

ある画像の上に別の画像を合成したい場合の初歩的な覚書。

UIGraphicsのImageContextを使う。これは要するにオフスクリーン描画領域。画面に表示する前の画像を扱うために確保するメモリ領域だな。
メモリ領域と言ってもwidthとheightのsizeで大きさを指定して作る。

使い方は難しくないけどちょっと変わっていて、Contextを作る
UIGraphicsBeginImageContext(CGSize)
UIGraphicsEndImageContext()
で挟まれた間に処理を書くことになる。

作られたContextは明確な変数に入るわけじゃないようだが、イメージに対してのdrawメソッドや、レイヤーに対してのrenderメソッドなどを書くとContextに対して描画が行われる。
例)
let img = UIImage(named: "ufoImage")
img?.draw(in: CGRect(origin: CGPoint.zero, size: view.frame.size))

上記はimgの画像Contextに描画するというものであり、img描画するわけじゃないからね。

いくつかUIImageを用意して順番にContextにdrawしてやれば、重ねられた合成画像が得られるわけだ。
いろいろ処理した後の画像は
UIGraphicsGetImageFromCurrentImageContext()
関数で得ることができる。(これは引数なし)

実際のコード

//画像の準備
let backImg = UIImage(named: "backImage"//背景画像
let ufoImg = UIImage(named: "ufoImage"//上に合成する画像
//オフスクリーンのContext作る
UIGraphicsBeginImageContext(view.frame.size)
//背景をContextに描画
backImage?.draw(in: CGRect(origin: CGPoint.zero, size: view.frame.size))
//合成する画像を位置を指定して描画
ufoImg?.draw(in: CGRect(x: view.frame.midX, y: view.frame.midY, width: 50, height: 50))
//context上に合成された画像を得る
let compositedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Contextは多用途

このContextを使うのは当然画像合成だけじゃなくいろいろ使える。
UIImageとCGImageなんかを取っ替え引っ替えいじってると画像の傾きがおかしくなったりすることもあるんだけど、そういう時にUIImageやCGImageなどの仕様上の呪縛(制約)から解放した画像を作り、あらためてUIImageを作り直してやるとかにも使える。

でかいサイズの画像を小さいサイズのContextに描画してやるとサイズ縮小ができたりとか。

まだよくわかってないことも多いので、いずれあらためて書くかも。

2017年3月1日水曜日

AVCapturePhotoOutputで少し難しくカメラアプリを作る

カメラで撮影するアプリを作るには UIImagePickerController を使うのが一番楽なんだけど、これだと細かいことができないので、 AVCapturePhotoOutput を使った書き方をしてみる。
iOS9までは別のクラス使ってたんだけど、iOS10からは非推奨になったので、これを使わにゃいかんのだ。

必要なFrameworkはAVFoundation。
以下に全コードを載せる。
撮影ボタンをUIButtonじゃなくて上部のツールバーにしてるけど、まあいいっしょ?
コードをシンプルにして構造をわかりやすくするため、極力エラー処理は省いてある。


カメラの使用とフォトライブラリへのアクセスをするので、あらかじめInfo.plistにそれに関するプライバシーの警告テキストを入力しておくこと。んでないと落ちる。
AVCaptureSessionクラス に入出力の情報をいろいろ設定し、startRunning() でカメラのライブビュー(プレビュー)表示を開始。ライブビュー映像はLayerを指定しないといけないようなので、imageViewにLayerを追加して表示している。
撮影ボタンを押すと takePhoto() が呼ばれ撮影。
撮影された画像はdelegateメソッドである capture(〜)メソッド の引数として受け取れるので、そこでDataからUIImageに変換。
それを普通に UIImageWriteToSavedPhotosAlbum()メソッド で写真ライブラリに保存している。

シミュレータじゃカメラ機能が使えないからiPadの実機で試したんだけど、撮影時のflashModeの行をコメントアウトしないと落ちてしまう。
iPadにはストロボないけど、.autoにしとけば自動で判断してくれそうなものだが。

コード

import UIKit
import AVFoundation

//キャプチャの入出力管理クラス
var captureSession:AVCaptureSession?
//静止画データの出力用
var stillImageOutput:AVCapturePhotoOutput?
//キャプチャ画像のプレビュー用
var videoPreviewLayer:AVCaptureVideoPreviewLayer?
//プレビュー表示用
var imageView = UIImageView()

class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //ツールバーに撮影ボタン追加
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 10, width: view.bounds.size.width, height: 44))
        let takePhotoButton = UIBarButtonItem(title: "撮影", style: .plain, target: self, action: #selector(ViewController.takePhoto))
        toolBar.items = [takePhotoButton]
        view.addSubview(toolBar)

        //プレビューに使うimageViewを作成
        imageView = UIImageView(frame: CGRect(x: 0, y: toolBar.frame.size.height, width: view.bounds.size.width, height: view.bounds.size.height))
        self.view.addSubview(imageView)

        
        //カメラの画像をキャプチャするクラスを作る
        captureSession = AVCaptureSession()
        //撮影解像度設定
        captureSession?.sessionPreset = AVCaptureSessionPresetPhoto
        
        //キャプチャーデバイスをビデオカメラに設定
        let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
        //ビデオを入力する。try!なのでエラー処理をせず、入力エラーの場合クラッシュ
        let input = try! AVCaptureDeviceInput(device: device)
        //キャプチャー管理のクラスに入力情報追加
        captureSession?.addInput(input)
        //キャプチャー管理のクラスに出力情報追加
        stillImageOutput = AVCapturePhotoOutput()
        captureSession?.addOutput(stillImageOutput)
        
        
        //プレビューレイヤー作成
        videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        //プレビューレイヤーのframe設定
        videoPreviewLayer?.frame = imageView.bounds
        //UIImageViewContentModeに相当
        videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
        //Layerbounds部分以外をマスクする
        videoPreviewLayer?.masksToBounds = true
        //imageViewlayerとしてプレビューレイヤーを乗せる
        imageView.layer.addSublayer(videoPreviewLayer!)

        
        //セッション開始
        captureSession?.startRunning()
        
    }
    
    func takePhoto() {
        let settingsForMonitoring = AVCapturePhotoSettings()
        //カメラのストロボモード
//        settingsForMonitoring.flashMode = .auto //コメントにしないと落ちる
        //カメラの手ぶれ補正
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true
        //最高解像度で撮影するか否か
        settingsForMonitoring.isHighResolutionPhotoEnabled = false
        //画面キャプチャ処理
        //撮影された画像はdelegateメソッドで処理する
        stillImageOutput?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }
    
    //AVCapturePhotoCaptureDelegateメソッド
    func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
        //結果がnilじゃなければ
        if let photoSampleBuffer = photoSampleBuffer {
            //キャプチャされたphotoSampleBufferの画像をJPEGとしてDataに直す
            //previewPhotoはサムネイル画像らしい
            let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
            //DataUIImageに変換
            let image = UIImage(data: photoData!)
            //写真ライブラリに保存
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
        }
    }

}


エラーチェックをする場合

     で挟まれたところを置き換える。

//キャプチャするデバイスをビデオに設定
        guard let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else {
            print("デバイスエラー")
            return
        }
        //ビデオを入力する。try!なのでエラー処理をせず、入力エラーの場合クラッシュ
        let input = try! AVCaptureDeviceInput(device: device)
        //追加可能な入力かどうか調べる
        if captureSession!.canAddInput(input) {
            //キャプチャー管理のクラスに入力情報追加
            captureSession?.addInput(input)
            //静止画の出力クラスを設定
            stillImageOutput = AVCapturePhotoOutput()
            //追加可能な出力かどうか調べる
            if captureSession!.canAddOutput(stillImageOutput) {
                //キャプチャー管理のクラスに出力情報追加
                captureSession?.addOutput(stillImageOutput)
                
                //プレビューレイヤー作成
                videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
                //guardは、nilでなければunwrapして返し、nilならそこで終る
                guard let _videoPreviewLayer = videoPreviewLayer else {
                    return
                }
                //プレビューレイヤーのframe設定
                _videoPreviewLayer.frame = imageView.bounds
                //UIImageViewで言うところのContentModeのこと
                _videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
                //Layerbounds部分以外をマスクする
                _videoPreviewLayer.masksToBounds = true
                //imageViewlayerとしてプレビューレイヤーを乗せる
                imageView.layer.addSublayer(_videoPreviewLayer)
                
                //セッション開始
                captureSession?.startRunning()
            } else {
                print("出力先エラー")
            }
        } else {
            print("入力元エラー")
        }
参考