おもちゃラボ

Unityで遊びを作ってます

【Unity】ROMアップロード時のエラー対処法

ROM(Androidならapk、iOSならipa)をアップロードしたときにつまずくポイントをまとめました。最後の関門なので、早く提出したいのに・・・!と一番イラつく(笑)ポイントなので、その手助けになれば!!

広告を使っていないのにIDFAのチェックで引っかかる(iOS)

ipaファイルをアップロードするときに、広告を使っていないのに次のような警告が出ることがあります。

「この App は広告 ID (IDFA) を使用しています。IDFA 使用状況の詳細を提供するか、IDFA を App から削除してバイナリを再送信する必要があります。」

Xcodeのプロジェクトを見てみると確かにAdsSupportのフレームワークがインポートされてしまっているようです。

f:id:nn_hokuson:20190905215539p:plain:w300

まずはUnityのメニューバーからWindow→Serviceウインドウを開いてUnityAdsの使用がOFFになっていることを確認しましょう。

f:id:nn_hokuson:20190905215822p:plain:w250

次にWIndow→Package ManagerからAdsのパッケージがインストールされていないかを確認します。インストールされていた場合にはUninstallボタンでアンインストールしましょう。

f:id:nn_hokuson:20190905215810p:plain:w450

ARCoreのmin_apk_versionでひっかかる(Android)

Vufoiraを使ったアプリケーションをサブミットしようとすると、次のようにARCoreのmin_apk_versionが指定されていないとエラーが出ることがあります。

com.google.ar.coreが指定されていますが、AndroidManifest.xmlファイルでARCore依存関係の最小バージョンコードcom.google.ar.core.min_apk_versionが指定されていません。

f:id:nn_hokuson:20190905220126p:plain:w600

この場合はまず、次のURLからARCoreの「core-1.9.0.aar」ファイルをダウンロードしてください(そのときの最新版でOKだと思います)

https://dl.google.com/dl/android/maven2/com/google/ar/core/1.9.0/core-1.9.0.aar

次に、ダウンロードした「core-1.9.0.aar」を、Plugins/Androidフォルダに配置してください(フォルダがない場合は作成します)

f:id:nn_hokuson:20190905220426p:plain:w300

64bit対応していないと怒られる(Android)

2019年8月からapkファイルの64bit対応が必須になりました。アプリが64bit対応していないと次のようにエラーが出ます。

このリリースはGoogle Playの64ビット要件に準拠していません。

以下の APK または App Bundle は 64 ビットのデバイスで利用できますが、
32 ビット向けネイティブ コードしか含まれていません。

アプリには 64 ビットと 32 ビットのネイティブ コードを含めます。
Android App Bundle 公開形式を使用して、各デバイスのアーキテクチャが自動的に必要なネイティブ
コードだけを受け取るようにします。これにより、アプリ全体のサイズが増大することを回避できます。

f:id:nn_hokuson:20190905220757p:plain:w500

Unityで64bit対応するにはPlayer Settings→ConfigurationのScripting Backendを「IL2CPP」に変更し、「ARM64」にチェックを入れます。

また、x86のチェックは必ず外すようにしましょう。これを外さないとx86_64のバイナリがない(そもそもUnity2019からは作成できなくなった)と怒られてしまいます。

f:id:nn_hokuson:20190905221229p:plain:w400

【iOS】画面のサイズに応じて文字のサイズを変更する

XcodeのAutoLayoutを使って文字を配置した場合、そのままだと画面サイズが変わっても同じフォントサイズで文字が表示されてしまいます。

これだと下図のように文字が画面からはみ出てしまったり、見切れてしまったりしまいます。ここでは、端末の画面サイズが変わったときに、自動的に文字のサイズもスケールする方法を紹介します。

f:id:nn_hokuson:20190806210143p:plain:w300 f:id:nn_hokuson:20190806210152p:plain:w206

文字を中央に固定する

まずはLabelを中央に配置して、文字サイズとAlignmentを調節します。ここではフォントサイズを120、Alignmentを中央寄せにしました。また、文字がLabel領域の中央に表示されるように、Baselineは「Align Centers」に設定しています。

f:id:nn_hokuson:20190806210256p:plain:w300

次に文字を画面中央に固定します(べつに固定するのは左上でもどこでも大丈夫です)。Labelを選択した状態で、Xcode右下のAlignメニューから「Horizontally in Container」と「Vertically in Container」にチェックを入れます。これでLabelが画面中央に表示されるようになります。
f:id:nn_hokuson:20190806210322p:plain:w350
また、画面サイズが変わってもLabelのアスペクトは固定したいので、「Add New Constraints」メニューから「Aspect Ratio」にチェックを入れます。
f:id:nn_hokuson:20190806210334p:plain:w350

自動的に文字サイズを変更する

画面サイズによって文字サイズを自動的に変更するには、Autoshrinkのドロップダウンメニューを「Minimum Font Scale」に設定します。これによってLabelの領域が小さくなると、そのサイズに応じて文字のフォントサイズも縮小されます。

f:id:nn_hokuson:20190806210451p:plain:w300

今の状態では画面のサイズが変わってもLabelの領域は変わりません。画面サイズに比例してLabelの領域も変わるように設定します。Xcodeの左側のLabelメニューからControlキーを押しながらViewにドラッグして「Equal Widths」を選択します。

f:id:nn_hokuson:20190806210502g:plain:w300

最後に今設定した「Proportional Width to SuperView」のMultiplierに「Labelの横幅 / 画面の幅」の値を設定します。ここでは画面幅が1024px、Labelの幅が614pxだったので614/1024=0.8を設定しています。

f:id:nn_hokuson:20190806210519p:plain:w300

これで画面サイズに応じて文字のサイズが自動的に拡縮されるようになりました。シミュレータで変更して試してみてください。

f:id:nn_hokuson:20190806210529p:plain:w300 f:id:nn_hokuson:20190806210537p:plain:w213

【iOS12対応】Visionを使って顔検出を行う

ここではVisionを使って顔を検出する方法を紹介します。VisionはiOS11から使える画像処理フレームワークです。ここではiOS12を使って説明します。

f:id:nn_hokuson:20190725211217j:plain:w550

Visonフレームワークとは

Vision は画像処理用のフレームワークです。ただOpenCVなどのように汎用的な画像処理ではなく、顔検出や文字検出など機械学習に特化したフレームワークになっています。

Visionフレームワークを使うと、次のようなものを簡単に検出することができます。それぞれのものについては個別に説明します。

  • 顔検出
  • 顔ランドマーク検出
  • 文字検出
  • 矩形検出
  • バーコード
  • オブジェクトトラッキング

顔検出の流れ

Visionフレームワークを使って顔検出をするには「リクエスト」と「リクエストハンドラ」の2つを使います。

リクエストハンドラ(VNImageRequestHandler)に処理したい画像とリクエスト内容を渡すことで、指定されたオブジェクトの検出処理が行われます。検出結果はObservationクラスを使って取得することができます。

f:id:nn_hokuson:20190725210217j:plain

リクエストには次のような種類があります。ここでは顔検出をしたいのでVNDetectFaceRectanglesRequestを使用しています。

VNDetectFaceRectanglesRequest 顔検出
VNDetectFaceLandmarksRequest 顔ランドマーク検出
VNDetectRectanglesRequest 矩形検出
VNDetectBarcodesRequest バーコード検出
VNDetectTextRectanglesRequest テキスト検出

それでは早速、写真から顔を検出するプログラムを作ってみましょう。

写真から顔を検出する

まずは読み込んだ写真から顔の位置を検出して、その周りに矩形を表示するプログラムを作ってみましょう。

Xcodeのプロジェクトを作成して、次のtest.jpgの画像をプロジェクトに追加します。追加できたらViewController.swiftに次のプログラムを入力してください。

import UIKit
import Vision

class ViewController: UIViewController {

    var image : UIImage!
    
    func drawRect(box:CGRect){
        let xRate : CGFloat = self.view.bounds.width / self.image.size.width
        let yRate : CGFloat = self.view.bounds.height / self.image.size.height
        
        let faceRect = CGRect(
            x: (box.minX) * self.image.size.width * xRate,
            y: (1 - box.maxY) * self.image.size.height * yRate,
            width: box.width * self.image.size.width * xRate,
            height: box.height * self.image.size.height * yRate
        )
        
        let faceTrackingView = UIView(frame: faceRect)
        faceTrackingView.backgroundColor = UIColor.clear
        faceTrackingView.layer.borderWidth = 1.0
        faceTrackingView.layer.borderColor = UIColor.green.cgColor
        self.view.addSubview(faceTrackingView)
        self.view.bringSubviewToFront(faceTrackingView)
    }
        
    override func viewDidLoad() {
        super.viewDidLoad()

		// 画像を読み込む
        self.image = UIImage(named: "test.jpg")
        
        // 顔検出用のリクエストを生成
        let request = VNDetectFaceRectanglesRequest { (request: VNRequest, error: Error?) in

            for observation in request.results as! [VNFaceObservation] {
                
                // 枠線を描画する
                self.drawRect(box:observation.boundingBox)
            }
        }
        
        // 顔検出開始
        if let cgImage = image.cgImage {
            let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
            try? handler.perform([request])
        }
    }
}

viewDidLoadメソッドの最初でVNDetectFaceRectanglesRequestで顔検出のリクエストを作成しています。

顔が見つかった場合はrequest.resultsにVNFaceObservationの配列として結果が格納されているので、ひとつずつ取り出してdrawRectメソッドで矩形を描画しています。

リクエストは作成しただけでは実行されません。最後にVNImageRequestHandlerを作って、performメソッドに先程の顔検出のリクエストを渡して実行しています。このタイミングで実際の顔検出が行われています。

動画から顔を検出する

一枚の写真から顔が検出できるようになったところで、次は動画から顔を検出してみます。といっても動画は時系列に画像が並んだものとみなせるので、顔検出の方法はほとんど同じです。ただiOSで動画を扱うにはAVCaptureSessionを使う必要があり、これが結構面倒です・・・

動画で顔検出する処理を追加したプログラムは、次のようになります。まずは全プログラムを載せておきます。

import UIKit
import Vision
import AVFoundation

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    private var _captureSession = AVCaptureSession()
    private var _videoDevice = AVCaptureDevice.default(for: AVMediaType.video)
    private var _videoOutput = AVCaptureVideoDataOutput()
    private var _videoLayer : AVCaptureVideoPreviewLayer? = nil
    private var rectArray:[UIView] = []
    var image : UIImage!
    
    func setupVideo( camPos:AVCaptureDevice.Position, orientaiton:AVCaptureVideoOrientation ){
        // カメラ関連の設定
        self._captureSession = AVCaptureSession()
        self._videoOutput = AVCaptureVideoDataOutput()
        self._videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: camPos)
        
        // Inputを作ってSessionに追加
        do {
            let videoInput = try AVCaptureDeviceInput(device: self._videoDevice!) as AVCaptureDeviceInput
            self._captureSession.addInput(videoInput)
        } catch let error as NSError {
            print(error)
        }
        
        // Outputを作ってSessionに追加
        self._videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String : Int(kCVPixelFormatType_32BGRA)]
        self._videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
        self._videoOutput.alwaysDiscardsLateVideoFrames = true
        self._captureSession.addOutput(self._videoOutput)
        
        for connection in self._videoOutput.connections {
            connection.videoOrientation = orientaiton
        }
        
        // 出力レイヤを作る
        self._videoLayer = AVCaptureVideoPreviewLayer(session: self._captureSession)
        self._videoLayer?.frame = UIScreen.main.bounds
        self._videoLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
        self._videoLayer?.connection?.videoOrientation = orientaiton
        self.view.layer.addSublayer(self._videoLayer!)
        
        // 録画開始
        self._captureSession.startRunning()
    }
    
    private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage {
        let imageBuffer: CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        let context = CGContext(data: CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0), width: CVPixelBufferGetWidth(imageBuffer), height: CVPixelBufferGetHeight(imageBuffer), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer), space: colorSpace, bitmapInfo: bitmapInfo)
        let imageRef = context!.makeImage()
        
        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        let resultImage: UIImage = UIImage(cgImage: imageRef!)
        return resultImage
    }
    
    func drawRect(box:CGRect){
        let xRate : CGFloat = self.view.bounds.width / self.image.size.width
        let yRate : CGFloat = self.view.bounds.height / self.image.size.height
        
        let faceRect = CGRect(
            x: (1 - box.maxX) * self.image.size.width * xRate,
            y: (1 - box.maxY) * self.image.size.height * yRate,
            width: box.width * self.image.size.width * xRate,
            height: box.height * self.image.size.height * yRate
        )
        
        let faceTrackingView = UIView(frame: faceRect)
        faceTrackingView.backgroundColor = UIColor.clear
        faceTrackingView.layer.borderWidth = 1.0
        faceTrackingView.layer.borderColor = UIColor.green.cgColor
        self.view.addSubview(faceTrackingView)
        self.view.bringSubviewToFront(faceTrackingView)
        rectArray.append(faceTrackingView)
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        
        DispatchQueue.main.async {
            self.image = self.imageFromSampleBuffer(sampleBuffer: sampleBuffer)
            // 顔検出用のリクエストを生成
            let request = VNDetectFaceRectanglesRequest { (request: VNRequest, error: Error?) in
                
                self.rectArray.forEach{ $0.removeFromSuperview() }
                self.rectArray.removeAll()
                
                for observation in request.results as! [VNFaceObservation] {
                    
                    // 枠線を描画する
                    self.drawRect(box:observation.boundingBox)
                }
            }
            
            // 顔検出開始
            if let cgImage = self.image.cgImage {
                let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
                try? handler.perform([request])
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupVideo(camPos: .front, orientaiton: .portrait)
    }
}

AVCaptureSessionを使うために、次のようにプログラムを変更しています。

  1. import AVFoundationを追加
  2. AVCaptureVideoDataOutputSampleBufferDelegateを継承
  3. setupVideoメソッドの中でAVCaptureSessionの初期化
  4. captureOutputデリゲートを宣言して、その中で顔検出

AVCaptureSessionには入力ソースと出力形式を指定する必要があります。入力にはiPhoneデバイスのカメラを指定します。一方出力には「静止画」「動画」「音声」などを選ぶことができます。
f:id:nn_hokuson:20190725202848j:plain
ここでは入力にはフロントカメラを指定しました。また、出力には動画のフレームデータがほしいのでAVCaptureVideoDataOutput指定しています。また、画像フォーマットやdelegateメソッド、表示の向きなどもあわせて指定します。

AVCaptureSessionが正しく設定できると、captureOutputメソッドが1フレームごとに呼び出されるようになります。この中で顔検出の処理を行って矩形を描画しています(ここは写真から顔検出するのとほぼ同じです)

プログラムができたらinfo.plistにPrivacy - Camera Usage Descriptionを追加してから(忘れたらアプリがクラッシュします!)実行してみてください。↓のような感じで動くと思います。