20200521のSwiftに関する記事は9件です。

UILabelの三点リーダーの位置のバラツキの調査

※調査記事の為、誤りが含まれている可能性があります。

はじめに

様々なアプリでUILabelが使われていますが、UILabelにおいて指定の領域を超えた文字数を保有する場合、領域外の文字は三点リーダー以降切り捨てられます。(見た目上は)
そこで、UILabelの三点リーダーの位置のバラつきが気になったので試してみました。

試した内容

下記で試した文字列の長さに対して、UILabelのwidthは短い長さに設定しました。

文字列 三点リーダーより前の文字列の文字数 三点リーダー
aaaaaaaaaaaaaaaaaa 2 スクリーンショット 2020-05-21 22.47.53.png (下)
aaaaaaaaaaaaaaaaaaあ 2 スクリーンショット 2020-05-21 22.47.53.png (下)
あaaaaaaaaaaaaaaa 2 スクリーンショット 2020-05-21 22.47.53.png (下)
aaaaaaaaaaaaaaaaあaaaaaaaa 2 スクリーンショット 2020-05-21 22.47.53.png (下)
あaああああああああああ 1 スクリーンショット 2020-05-21 22.47.53.png (下)
あああああああああ 1 スクリーンショット 2020-05-21 22.46.29.png (中)
ああaa 1 スクリーンショット 2020-05-21 22.46.29.png (中)

仮説

文字列の末尾がマルチバイト文字か否かで三点リーダーの位置が変わると考えていました。
しかし、上記の結果を見る限りでは「三点リーダーで切り捨てられる最初の文字」がマルチバイト文字か否かで三点リーダーの位置が変わります。

  • シングルバイト文字(アルファベットや数字など):下
  • マルチバイト文字(ひらがなや漢字など):中

最後に

色々なアプリを覗いてみると、三点リーダーの位置がバラバラなことが分かるので見てみると良いかも知れません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITextViewの文字を複数リンク化させる方法

はじめに

スクリーンショット 2020-05-21 19.19.21.png

iOSが意外に苦手なリンク化
HTMLみたいにタグで囲った範囲をリンク化する!みたいに出来たら便利なのですが、出来ないので

リンク化したい文字列の場所とタップした位置でゲームみたいに当たり判定でリンクをタップしたか判定します!

ViewController + Storyboardで解説

Storyboard(Xibでも可)にUITextViewを貼り付ける

スクリーンショット 2020-05-21 16.35.01.png

制約も適当に張ります。
スクリーンショット 2020-05-21 16.35.15.png

UITextViewにTapGestureを張ります!
スクリーンショット 2020-05-21 18.34.26.png

スクリーンショット 2020-05-21 18.35.34.png

こんな感じ!

スクリーンショット 2020-05-21 18.38.35.png

Storyboardとコードをつなげる

Controlキーを押しながら、TextViewをひっぱりコードに張りますー
(ちなみにエディタを分割して開くには、ファイルをoptionキーを押しながらクリックします)

スクリーンショット 2020-05-21 16.38.20.png

TapGestureもコードにつなげておきます〜

スクリーンショット 2020-05-21 18.43.36.png

以下のようになりました!

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var messageTextView: UITextView!
    override func viewDidLoad() {
        super.viewDidLoad()

    }
    @IBAction func didTapMessageView(_ sender: UITapGestureRecognizer) {
    }
}

本題

messageTextViewの初期設定をしてあげます

仕様にもよりますが、以下の制御があったほうが理想だと思います。

messageTextView.isUserInteractionEnabled = true
messageTextView.isEditable = false
messageTextView.isSelectable = false
messageTextView.isScrollEnabled = false
   @IBOutlet weak var messageTextView: UITextView! {
        didSet {
            messageTextView.isUserInteractionEnabled = true
            messageTextView.isEditable = false
            messageTextView.isSelectable = false
            messageTextView.isScrollEnabled = false
            let mutableAttributedString = NSMutableAttributedString()
            let normalAttirbutes: [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.gray,
                .font: UIFont.monospacedSystemFont(ofSize: 18, weight: .medium)
            ]
            let linkAttributes:  [NSAttributedString.Key: Any] = [
                .foregroundColor: UIColor.link,
                .underlineStyle: NSUnderlineStyle.single.rawValue,
                .font: UIFont.monospacedSystemFont(ofSize: 22, weight: .medium)
            ]

            mutableAttributedString.append(
                NSAttributedString(
                    string: "吾輩わがはいは猫である。",
                    attributes: normalAttirbutes
                )
            )
            mutableAttributedString.append(
                NSAttributedString(
                    string: "名前",
                    attributes: linkAttributes
                )
            )
            mutableAttributedString.append(
                NSAttributedString(
                    string: "はまだ無い。",
                    attributes: normalAttirbutes
                )
            )
            mutableAttributedString.append(
                NSAttributedString(
                    string: "どこで",
                    attributes: linkAttributes
                )
            )
            mutableAttributedString.append(
                NSAttributedString(
                    string: "生れたかとんと見当けんとうがつかぬ。",
                    attributes: normalAttirbutes
                )
            )
            messageTextView.attributedText = mutableAttributedString
        }
    }

次にタップ時の挙動を記述していきます。

    @IBAction func didTapMessageView(_ sender: UITapGestureRecognizer) {
        guard let text = messageTextView.text else { return }
        let name = "名前"
        let pos = "どこで"

        let nameRange = (text as NSString).range(of: name)
        let posRange = (text as NSString).range(of: pos)

        let location = sender.location(in: messageTextView)
        let textPosition = messageTextView.closestPosition(to: location)
        let tapPosition = messageTextView.offset(from: messageTextView.beginningOfDocument, to: textPosition!)

        if NSLocationInRange(tapPosition, nameRange) {
            print("たま")
        }

        if NSLocationInRange(tapPosition, posRange) {
            print("縁側")
        }
    }

結果

3j0z2-rnaio.gif

終わり

ポイントはNSLocationInRangeで毎回当たり判定的なものを行なって毎回処理を走らせるところです!

これをUIViewControllerRepresentableすればSwiftUIでも行けるのかも?
今度試してみます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

URLsessionを用いたHTTPリクエストの方法(Swift, Xcode)

はじめに

APIKitやAlamofireなどのライブラリを使わず標準のURLsessionを用いたHTTPリクエストを実行するを簡単にまとめました。

1. URLsession手順

  1. URLを生成
  2. URLRequestを生成
  3. 必要なパラメーターを付与
  4. 通信を行う
  5. レスポンスで受け取ったdata, response, errorの処理を行う

2.基本のコード

Get

基本は以下のコードのようになります。

let url = URL(string: "https://hogehoge.hoge")!  //URLを生成
var request = URLRequest(url: url)               //Requestを生成
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in  //非同期で通信を行う
    guard let data = data else { return }
    do {
        let object = try JSONSerialization.jsonObject(with: data, options: [])  // DataをJsonに変換
        print(object)
    } catch let error {
        print(error)
    }
}
task.resume()

Post, Put

PostやPutにしたい場合はrequest.httpMethodを使います。

let url = URL(string: "https://hogehoge.hoge")! 
var request = URLRequest(url: url)
request.httpMethod = "POST"      // Postリクエストを送る(このコードがないとGetリクエストになる) 
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in  
    guard let data = data else { return }
    do {
        let object = try JSONSerialization.jsonObject(with: data, options: [])
        print(object)
    } catch let error {
        print(error)
    }
}
task.resume()

3. Body, Header, Queryを付与したいときのコード

Bodyを付与(Get)

let url = URL(string: "https://hogehoge.hoge")! 
var request = URLRequest(url: url)
request.httpBody = "email=hoge@hoge.com&username=hogehoge".data(using: .utf8)  // 例)emailとpasswordを付与する場合

Headerを付与(Get)

let url = URL(string: "https://hogehoge.hoge")! 
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")  //jsonでヘッダーに付与
request.allHTTPHeaderFields = ["Token": "snsjetnglsdfnm"]                 // 例) ヘッダーにトークンを付与する場合

Queryを付与(Get)

var urlComponents = URLComponents(string: "https://hogehoge.hoge")!  //URLComponentsでURLを生成
        urlComponents.queryItems = [
            URLQueryItem(name: "email", value: "hoge@hoge.com"),      // 例) emailとpasswordを付与
            URLQueryItem(name: "username", value: "hogehoge")         //クエリを追加したければ、ここに書いていく
        ]
var request = URLRequest(url: urlComponents.url!)

→ 生成されるリクエスト "https://hogehoge.hoge/email=hoge@hoge.com&username=hogehoge"

参考

下記のサイトを見ながら実装したので詳細はこちらをご確認ください。

URLSessionを用いた通信 - GitHub
HTTP GETとPOST(Swift)
【Swift】URLSessionまとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】@testable import no such module “xxx” というエラーが出る

Xcodeでユニットテストを実行しようとしたらタイトルのエラーが出てしまった。
そんなモジュールは存在しないよと怒られてます。

:tools: TestのTargetの「Host Application」が設定されていない

スクリーンショット 2020-05-21 16.11.08.png

上記の、タブ画面でHostApplicationが「None」になっていないか確かめる。
もしなっていたたら現在自分が開発中のアプリをターゲットにしてあげる。

:computer: プロジェクト名などを途中で変更した場合

テストファイル内の

@testable import test

上記でいう「test」の部分は自動で更新されない。。
ので自分で修正してあげる必要がある。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面サイズに合わせてパーツの大きさを変える(初心者向け)

こんにちは。プログラミング初心者兼Swift初心者です。

storyboardのオートレイアウトで画面サイズが変わっても、部品の位置はある程度決めることができますが、大きさを変えることができないので「小さい画面だとキツキツ、大きい画面だと余白ができる」なんてことがありました。
そこで画面サイズを元にして、動的に部品サイズを変更することができたのでコードを載せておきます。

    //今回はImageViewの大きさを変更します
    @IBOutlet weak var imageView: UIImageView!
    //画面サイズを取得
    let screenSize = UIScreen.main.bounds.size

    override func viewDidLoad() {
        super.viewDidLoad()

        //imageViewの横幅を画面サイズの80%にする
        let ivWidth = screenSize.width * 0.8
        //imageViewの縦幅は横幅の0.666....倍
        let ivHeight = ivWidth * 0.66
        //あとから左右中央にするのでy座標と大きさをここで決める
        imageView.frame = CGRect(x: 0,
                                 y: screenSize.height / 6,
                                 width: ivWidth,
                                 height: ivHeight)
        //左右中央に指定
        imageView.center.x = screenSize.width / 2
    }

簡略化するためにUIImageViewのアスペクト比や画面サイズを元に定数で書いていますが、変数に格納したほうがいいですね笑

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「VNDocumentCameraViewController」を使ってドキュメントをスキャンし、「VNDetectTextRectanglesRequest」を使って日本語の文字画像を抽出

「Vision」について:

「Vision」は、画像や動画などの入力を処理できる多くの機械学習アルゴリズムを含むフレームワークです。テキストを処理する既存の関数「VNRecognizeTextRequest」がありますが、これは英語しかサポートしませんのでご注意ください。また、「Qitta」で「VNRecognizeTextRequest」を検索すると、それに関する記事がすでに出ています。

この記事では、検出された日本語テキストの周りに長方形を描く方法が説明されています。

フレームワークのコンポーネント

VNDocumentCameraViewController (VisionKit):

これはiOSフレームワークに組み込まれているスキャナーサポートで、用紙の長方形の領域を自動的に検出します。それからユーザーはボタンをクリックしてドキュメントの画像を撮影し、その画像データをデリゲートで受け取ることができます。

VNDetectTextRectanglesRequest

「Vision」フレームワークの関数で、テキストがある領域の境界ボックスを取得できます。

VNDetectTextRectanglesRequest を使用する理由

  1. VNRecognizeTextRequest は日本語をサポートしていないためです
  2. VNDetectTextRectanglesRequest で認識された領域を切り抜き、その結果を独自のテキスト認識アルゴリズムに入力できます。この場合、VNDetectTextRectanglesRequest に文字ごとに結果を表示するように要求すれば、各文字を認識できます。

さあ始めましょう。

ドキュメントをスキャンする

VNDocumentCameraViewController の実装は簡単です:

import VisionKit
@IBAction func actionPresentVision(){
    let documentCameraViewController = VNDocumentCameraViewController()
    documentCameraViewController.delegate = self
    present(documentCameraViewController, animated: true)
}

ユーザーが撮影した画像を受け取るプログラムデリゲートも実装する必要があります:

extension ViewController: VNDocumentCameraViewControllerDelegate {

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        if let firstImage = scan.imageOfPage(at: 0).cgImage {
            //TODO
            processImage(input: firstImage)
            controller.dismiss(animated: true, completion: nil)
        }
    }

    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        controller.dismiss(animated: true, completion: nil)
    }

}

押すと "actionPresentVision" を呼び出す UIButton を Storyboard に追加します。そして画面に画像を表示する processImage 関数を設定します。

@IBOutlet weak var imageView: UIImageView!

func processImage(input: CGImage) {
    let image = UIImage(cgImage: input)
    imageView.image = image
}

ezgif-7-b472f5ddaf87.gif

(Keep Scan -> Save)

画像の部分ができたので、機械学習の部分にとりかかりましょう:

機械学習を使ってテキストを認識する

まず、フレームワークをインポートします:

import Vision

次に、前回の記事で書いたように、機械学習による結果を処理するためのハンドラーを設定し、VNImageRequestHandler を使って画像データをリクエストにインプットする必要があります。

アニメ画像の昼/夜認識システムの作成:(2/3) そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

前述の記事では VNCoreMLRequest を 使っていますが、ここでは VNDetectTextRectanglesRequest を使うことにもご注意ください:

func processImage(input: CGImage) {
    let request = VNDetectTextRectanglesRequest { (request, error) in
        //ここに結果が表示されます
        if let results = request.results as? [VNTextObservation] {
            print(results)
        }
    }
    //次にリクエストに画像を受け渡します
    let handler = VNImageRequestHandler(cgImage: input, options: [:])
    DispatchQueue.global(qos: .userInteractive).async {
        do {
            try handler.perform([request])
        } catch {
             print(error)
        }
    }
}

こちらが得られた結果です:

[

<VNTextObservation: 0x282a34fa0> CCD5518B-FC57-42E2-8475-7E4883F3782A requestRevision=1 confidence=1.000000 boundingBox=[0.186391, 0.678969, 0.568047, 0.0543175], 

<VNTextObservation: 0x282a348c0> DCEC8A13-FC2F-4D55-A2C0-D88EAEAF304C requestRevision=1 confidence=1.000000 boundingBox=[0.363905, 0.550836, 0.224852, 0.0557103], 

<VNTextObservation: 0x282a34d20> 93FEE50E-D6B6-49F4-983E-C8025D45A4EC requestRevision=1 confidence=1.000000 boundingBox=[0.381657, 0.39624, 0.198225, 0.051532], 

<VNTextObservation: 0x282a34aa0> 2215CEAB-09B5-40E2-86C0-5DC33F354057 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.236072, 0.210059, 0.051532], 

<VNTextObservation: 0x282a34c80> E88AA596-EF6E-4CD1-B19B-AC7EE7A30936 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.0759053, 0.215976, 0.0501393]

]

紙にはちょうど5つの単語が書かれています!各「VNTextObservation」は画像内の各単語のバウンディングボックスを表しています。

あとは、結果を画面上に視覚的に表示できるようにするだけです。:

視覚的に表示

func drawBoundingBox(forResult: VNTextObservation) {
    let outline = CALayer()
    //バウンディングボックスの座標はパーセンテージとして与えられます。実際の画面の座標に変換する必要があります
    let x = forResult.topLeft.x * imageView.frame.width
    let y = (1 - forResult.topLeft.y) * imageView.frame.height
    ///横幅と高さは「boundingBox」から取得できます
    let width = forResult.boundingBox.width * imageView.frame.width
    let height = forResult.boundingBox.height * imageView.frame.height
    outline.frame = CGRect(x: x, y: y, width: width, height: height)
    outline.borderColor = UIColor.green.cgColor
    outline.borderWidth = 3
    imageView.layer.addSublayer(outline)
}

また、「drawBoundingBox」を呼び出すようにリクエストを変更するのを忘れないでください。

let request = VNDetectTextRectanglesRequest { (request, error) in
    if let results = request.results as? [VNTextObservation] {
        for result in results {
            DispatchQueue.main.async {
                self.imageView.image = UIImage(cgImage: input)
                self.drawBoundingBox(forResult: result)
            }
        }
    }
}

こちらが結果です。:

Screen Shot 2020-05-12 at 1.06.19 PM.png

パラメーターを「forResult.characterBoxes」に変更すれば、「drawBoundingBox」の同じコードを使って各文字のバウンディングボックスを描画することもできます。

一部のバウンディングボックスが表示されない場合がありますが、これは正常です。機械学習には限界があり、光の当たり具合が適切ではないと不正確なデータを返してしまうことがあるからです。

コード

コードはこちらにあります: https://github.com/mszopensource/VisionTextDetection

次のステップ

日本語の文字を認識する機械学習モデルをトレーニングし、「forResult.characterBoxes」の各文字の画像を用いて各文字を認識させることができます。その方法は既に別記事で執筆済みです:

1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VNDocumentCameraViewController を使ってドキュメントをスキャンし、VNDetectTextRectanglesRequest を使って日本語の文字画像を抽出

Screen Shot 2020-05-12 at 1.06.19 PM.png

「Vision」について:

「Vision」は、画像や動画などの入力を処理できる多くの機械学習アルゴリズムを含むフレームワークです。テキストを処理する既存の関数「VNRecognizeTextRequest」がありますが、これは英語しかサポートしませんのでご注意ください。また、「Qitta」で「VNRecognizeTextRequest」を検索すると、それに関する記事がすでに出ています。

この記事では、検出された日本語テキストの周りに長方形を描く方法が説明されています。

フレームワークのコンポーネント

VNDocumentCameraViewController (VisionKit):

これはiOSフレームワークに組み込まれているスキャナーサポートで、用紙の長方形の領域を自動的に検出します。それからユーザーはボタンをクリックしてドキュメントの画像を撮影し、その画像データをデリゲートで受け取ることができます。

VNDetectTextRectanglesRequest

「Vision」フレームワークの関数で、テキストがある領域の境界ボックスを取得できます。

VNDetectTextRectanglesRequest を使用する理由

  1. VNRecognizeTextRequest は日本語をサポートしていないためです
  2. VNDetectTextRectanglesRequest で認識された領域を切り抜き、その結果を独自のテキスト認識アルゴリズムに入力できます。この場合、VNDetectTextRectanglesRequest に文字ごとに結果を表示するように要求すれば、各文字を認識できます。

さあ始めましょう。

ドキュメントをスキャンする

VNDocumentCameraViewController の実装は簡単です:

import VisionKit
@IBAction func actionPresentVision(){
    let documentCameraViewController = VNDocumentCameraViewController()
    documentCameraViewController.delegate = self
    present(documentCameraViewController, animated: true)
    //以前に追加したレイヤーを削除する
    for layer in imageView.layer.sublayers ?? [] {
        layer.removeFromSuperlayer()
    }
}

ユーザーが撮影した画像を受け取るプログラムデリゲートも実装する必要があります:

extension ViewController: VNDocumentCameraViewControllerDelegate {

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        if let firstImage = scan.imageOfPage(at: 0).cgImage {
            //TODO
            processImage(input: firstImage)
            controller.dismiss(animated: true, completion: nil)
        }
    }

    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        controller.dismiss(animated: true, completion: nil)
    }

}

押すと "actionPresentVision" を呼び出す UIButton を Storyboard に追加します。そして画面に画像を表示する processImage 関数を設定します。

@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()
    imageView.contentMode = .scaleToFill
}

func processImage(input: CGImage) {
    let image = UIImage(cgImage: input)
    imageView.image = image
}

ezgif-7-b472f5ddaf87.gif

(Keep Scan -> Save)

画像の部分ができたので、機械学習の部分にとりかかりましょう:

機械学習を使ってテキストを認識する

まず、フレームワークをインポートします:

import Vision

次に、前回の記事で書いたように、機械学習による結果を処理するためのハンドラーを設定し、VNImageRequestHandler を使って画像データをリクエストにインプットする必要があります。

アニメ画像の昼/夜認識システムの作成:(2/3) そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

前述の記事では VNCoreMLRequest を 使っていますが、ここでは VNDetectTextRectanglesRequest を使うことにもご注意ください:

func processImage(input: CGImage) {
    let request = VNDetectTextRectanglesRequest { (request, error) in
        //ここに結果が表示されます
        if let results = request.results as? [VNTextObservation] {
            print(results)
        }
    }
    //次にリクエストに画像を受け渡します
    let handler = VNImageRequestHandler(cgImage: input, options: [:])
    DispatchQueue.global(qos: .userInteractive).async {
        do {
            try handler.perform([request])
        } catch {
             print(error)
        }
    }
}

こちらが得られた結果です:

[

<VNTextObservation: 0x282a34fa0> CCD5518B-FC57-42E2-8475-7E4883F3782A requestRevision=1 confidence=1.000000 boundingBox=[0.186391, 0.678969, 0.568047, 0.0543175], 

<VNTextObservation: 0x282a348c0> DCEC8A13-FC2F-4D55-A2C0-D88EAEAF304C requestRevision=1 confidence=1.000000 boundingBox=[0.363905, 0.550836, 0.224852, 0.0557103], 

<VNTextObservation: 0x282a34d20> 93FEE50E-D6B6-49F4-983E-C8025D45A4EC requestRevision=1 confidence=1.000000 boundingBox=[0.381657, 0.39624, 0.198225, 0.051532], 

<VNTextObservation: 0x282a34aa0> 2215CEAB-09B5-40E2-86C0-5DC33F354057 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.236072, 0.210059, 0.051532], 

<VNTextObservation: 0x282a34c80> E88AA596-EF6E-4CD1-B19B-AC7EE7A30936 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.0759053, 0.215976, 0.0501393]

]

紙にはちょうど5つの単語が書かれています!各「VNTextObservation」は画像内の各単語のバウンディングボックスを表しています。

あとは、結果を画面上に視覚的に表示できるようにするだけです。:

視覚的に表示

func drawBoundingBox(forResult: VNTextObservation) {
    let outline = CALayer()
    //バウンディングボックスの座標はパーセンテージとして与えられます。実際の画面の座標に変換する必要があります
    let x = forResult.topLeft.x * imageView.frame.width
    let y = (1 - forResult.topLeft.y) * imageView.frame.height
    ///横幅と高さは「boundingBox」から取得できます
    let width = forResult.boundingBox.width * imageView.frame.width
    let height = forResult.boundingBox.height * imageView.frame.height
    outline.frame = CGRect(x: x, y: y, width: width, height: height)
    outline.borderColor = UIColor.green.cgColor
    outline.borderWidth = 3
    imageView.layer.addSublayer(outline)
}

また、「drawBoundingBox」を呼び出すようにリクエストを変更するのを忘れないでください。

let request = VNDetectTextRectanglesRequest { (request, error) in
    if let results = request.results as? [VNTextObservation] {
        for result in results {
            DispatchQueue.main.async {
                self.imageView.image = UIImage(cgImage: input)
                self.drawBoundingBox(forResult: result)
            }
        }
    }
}

こちらが結果です。:

Screen Shot 2020-05-12 at 1.06.19 PM.png

パラメーターを「forResult.characterBoxes」に変更すれば、「drawBoundingBox」の同じコードを使って各文字のバウンディングボックスを描画することもできます。

一部のバウンディングボックスが表示されない場合がありますが、これは正常です。機械学習には限界があり、光の当たり具合が適切ではないと不正確なデータを返してしまうことがあるからです。

コード

コードはこちらにあります: https://github.com/mszopensource/VisionTextDetection

次のステップ

日本語の文字を認識する機械学習モデルをトレーニングし、「forResult.characterBoxes」の各文字の画像を用いて各文字を認識させることができます。その方法は既に別記事で執筆済みです:

1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VNDetectTextRectanglesRequest: 機械学習を用いて日本語の単語の領域を検出します。

Screen Shot 2020-05-12 at 1.06.19 PM.png

「Vision」について:

「Vision」は、画像や動画などの入力を処理できる多くの機械学習アルゴリズムを含むフレームワークです。テキストを処理する既存の関数「VNRecognizeTextRequest」がありますが、これは英語しかサポートしませんのでご注意ください。また、「Qitta」で「VNRecognizeTextRequest」を検索すると、それに関する記事がすでに出ています。

この記事では、検出された日本語テキストの周りに長方形を描く方法が説明されています。

フレームワークのコンポーネント

VNDocumentCameraViewController (VisionKit):

これはiOSフレームワークに組み込まれているスキャナーサポートで、用紙の長方形の領域を自動的に検出します。それからユーザーはボタンをクリックしてドキュメントの画像を撮影し、その画像データをデリゲートで受け取ることができます。

VNDetectTextRectanglesRequest

「Vision」フレームワークの関数で、テキストがある領域の境界ボックスを取得できます。

VNDetectTextRectanglesRequest を使用する理由

  1. VNRecognizeTextRequest は日本語をサポートしていないためです
  2. VNDetectTextRectanglesRequest で認識された領域を切り抜き、その結果を独自のテキスト認識アルゴリズムに入力できます。この場合、VNDetectTextRectanglesRequest に文字ごとに結果を表示するように要求すれば、各文字を認識できます。

さあ始めましょう。

ドキュメントをスキャンする

VNDocumentCameraViewController の実装は簡単です:

import VisionKit
@IBAction func actionPresentVision(){
    let documentCameraViewController = VNDocumentCameraViewController()
    documentCameraViewController.delegate = self
    present(documentCameraViewController, animated: true)
    //以前に追加したレイヤーを削除する
    for layer in imageView.layer.sublayers ?? [] {
        layer.removeFromSuperlayer()
    }
}

ユーザーが撮影した画像を受け取るプログラムデリゲートも実装する必要があります:

extension ViewController: VNDocumentCameraViewControllerDelegate {

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        if let firstImage = scan.imageOfPage(at: 0).cgImage {
            //TODO
            processImage(input: firstImage)
            controller.dismiss(animated: true, completion: nil)
        }
    }

    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        controller.dismiss(animated: true, completion: nil)
    }

}

押すと "actionPresentVision" を呼び出す UIButton を Storyboard に追加します。そして画面に画像を表示する processImage 関数を設定します。

@IBOutlet weak var imageView: UIImageView!

override func viewDidLoad() {
    super.viewDidLoad()
    imageView.contentMode = .scaleToFill
}

func processImage(input: CGImage) {
    let image = UIImage(cgImage: input)
    imageView.image = image
}

ezgif-7-b472f5ddaf87.gif

(Keep Scan -> Save)

画像の部分ができたので、機械学習の部分にとりかかりましょう:

機械学習を使ってテキストを認識する

まず、フレームワークをインポートします:

import Vision

次に、前回の記事で書いたように、機械学習による結果を処理するためのハンドラーを設定し、VNImageRequestHandler を使って画像データをリクエストにインプットする必要があります。

アニメ画像の昼/夜認識システムの作成:(2/3) そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

前述の記事では VNCoreMLRequest を 使っていますが、ここでは VNDetectTextRectanglesRequest を使うことにもご注意ください:

func processImage(input: CGImage) {
    let request = VNDetectTextRectanglesRequest { (request, error) in
        //ここに結果が表示されます
        if let results = request.results as? [VNTextObservation] {
            print(results)
        }
    }
    //次にリクエストに画像を受け渡します
    let handler = VNImageRequestHandler(cgImage: input, options: [:])
    DispatchQueue.global(qos: .userInteractive).async {
        do {
            try handler.perform([request])
        } catch {
             print(error)
        }
    }
}

こちらが得られた結果です:

[

<VNTextObservation: 0x282a34fa0> CCD5518B-FC57-42E2-8475-7E4883F3782A requestRevision=1 confidence=1.000000 boundingBox=[0.186391, 0.678969, 0.568047, 0.0543175], 

<VNTextObservation: 0x282a348c0> DCEC8A13-FC2F-4D55-A2C0-D88EAEAF304C requestRevision=1 confidence=1.000000 boundingBox=[0.363905, 0.550836, 0.224852, 0.0557103], 

<VNTextObservation: 0x282a34d20> 93FEE50E-D6B6-49F4-983E-C8025D45A4EC requestRevision=1 confidence=1.000000 boundingBox=[0.381657, 0.39624, 0.198225, 0.051532], 

<VNTextObservation: 0x282a34aa0> 2215CEAB-09B5-40E2-86C0-5DC33F354057 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.236072, 0.210059, 0.051532], 

<VNTextObservation: 0x282a34c80> E88AA596-EF6E-4CD1-B19B-AC7EE7A30936 requestRevision=1 confidence=1.000000 boundingBox=[0.372781, 0.0759053, 0.215976, 0.0501393]

]

紙にはちょうど5つの単語が書かれています!各「VNTextObservation」は画像内の各単語のバウンディングボックスを表しています。

あとは、結果を画面上に視覚的に表示できるようにするだけです。:

視覚的に表示

func drawBoundingBox(forResult: VNTextObservation) {
    let outline = CALayer()
    //バウンディングボックスの座標はパーセンテージとして与えられます。実際の画面の座標に変換する必要があります
    let x = forResult.topLeft.x * imageView.frame.width
    let y = (1 - forResult.topLeft.y) * imageView.frame.height
    ///横幅と高さは「boundingBox」から取得できます
    let width = forResult.boundingBox.width * imageView.frame.width
    let height = forResult.boundingBox.height * imageView.frame.height
    outline.frame = CGRect(x: x, y: y, width: width, height: height)
    outline.borderColor = UIColor.green.cgColor
    outline.borderWidth = 3
    imageView.layer.addSublayer(outline)
}

また、「drawBoundingBox」を呼び出すようにリクエストを変更するのを忘れないでください。

let request = VNDetectTextRectanglesRequest { (request, error) in
    if let results = request.results as? [VNTextObservation] {
        for result in results {
            DispatchQueue.main.async {
                self.imageView.image = UIImage(cgImage: input)
                self.drawBoundingBox(forResult: result)
            }
        }
    }
}

こちらが結果です。:

Screen Shot 2020-05-12 at 1.06.19 PM.png

パラメーターを「forResult.characterBoxes」に変更すれば、「drawBoundingBox」の同じコードを使って各文字のバウンディングボックスを描画することもできます。

一部のバウンディングボックスが表示されない場合がありますが、これは正常です。機械学習には限界があり、光の当たり具合が適切ではないと不正確なデータを返してしまうことがあるからです。

コード

コードはこちらにあります: https://github.com/mszopensource/VisionTextDetection

次のステップ

日本語の文字を認識する機械学習モデルをトレーニングし、「forResult.characterBoxes」の各文字の画像を用いて各文字を認識させることができます。その方法は既に別記事で執筆済みです:

1.「Core ML」モデルを「Create ML」で既存のラベル付けされたアニメ画像を入力として用いてトレーニングする。
2. そのモデルと「Vision」フレームワークを用いて新規画像からラベルを取得する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】SVProgressHUDの表示がiOS13以上だとおかしくなる

結構古くからある「SVProgressHUD」というローダーのライブラリがあります。
癖が無くて使いやすいのですが、iOS13.0で動かしてみたら画面中央では無く何故か左上の隅っこに表示されてしまいました。

72616955-f5fd9580-395d-11ea-93e3-9d0310f525bb.png

対応策

AppDelegate.swift

static var standard: AppDelegate {
  return UIApplication.shared.delegate as! AppDelegate
}

SceneDelegate.swift

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
  guard let _ = (scene as? UIWindowScene) else { return }
  AppDelegate.standard.window = window
}

以上のように変更するとiOS13系でもローディングが中央に表示されるようになった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む