20201025のiOSに関する記事は11件です。

OKI AE2100 & Node-REDでローコードIoTしてみた。その4実践編 2

Node-REDでローコードIoT実践編 2
MQTT Pub/Sub

はじめに

本記事はOKIの「AIエッジコンピューター」と呼ばれる AE2100 というゲートウェイ製品にNode-REDを載せて、ローコードIoTのプラットフォームにしようという記事の その4 Node-REDでローコードIoT実践編 2 MQTT Pub/Sub です。

その1 PythonでRS-485圧力センサーシミュレータ準備編
その2 DockerでローコードIoTプラットフォーム構築編
その3 Node-REDでローコードIoT実践編 1 ダッシュボード作成
その4 Node-REDでローコードIoT実践編 2 MQTT Pub/Sub (本記事)

前編では RS-485入力の圧力値をNode-RED でローカルに作ったダッシュボードにリアルタイム表示しました。
もうそこまでで十分なわけですが、いちおうIoT関連の記事なので、ネットワークを利用するところまでやらせてください!

こんなことやります

image.png

本編で行うのは最後の 4 MQTT Pub/Sub になります。
RS-485で受信した圧力値をクラウドに飛ばしてIoTらしいアプリに仕上げます。
使用するネットワークプロトコルはMQTTです。

MQTT

Node-RED 経験者なら、ほぼ間違いなくMQTTを使ったことがあるでしょうが、ここではMQTT初心者向けに少しだけ解説します。
MQTTとは、パブリッシュ/サブスクライブ型のメッセージキューです。
サーバーであるMQTTブローカーを仲介者として、複数のクライアントがサブスクライブ(購読)とパブリッシュ(発行)に分かれ、トピック名をキーワードとしてメッセージの送受信を行います。

image.png

1つのクライアントがパブリッシュとサブスクライブの両方を行うこともあります。
Node-RED ならば、MQTT、Node-RED 両方の初心者でも、設定だけで簡単に MQTT を試すことができます。

なおMQTTブローカーには、test.mosquitto.org を使わせていただきます。

Node-RED フロー作成

Node-RED のフローエディタ構成を再掲しました。
ワークスペース内のフローがこれから作成するフローです。
image.png

流量制限ノード

まず上図のフローの左下に追加した "1 data/10 sec" ノードですが、これは毎秒受信する圧力センサーのデータを10秒に1回に流量制限するものです。
無料で使わせていただくMQTTブローカーに毎秒データを送りつけるのは気が引けるので、遠慮して10秒に1回にします。

パレットにあるこの delay ノードを使用します。
image.png

これをワークスペースに置いて、ダブルクリックして以下のように設定して完了します。
image.png
このように流量の制限が設定だけで簡単にできてしまいます。

MQTTパブリッシュノード

次に、MQTTブローカーにセンサーデータをパブリッシュするノードを設定します。
パレットからこの mqtt out ノードを掴んで、ワークスペースにドロップします。
image.png

ダブルクリックして設定します。
まずは鉛筆アイコンをクリックして、MQTTブローカーの設定画面に移行します。下の右にある設定画面が開きます。
名前はとりあえず test.mosquitto.org としておきましょう。
サーバは今回は test.mosquitto.org と指定する必要があります。
ポートはデフォルトの1883でOKです。
クライアント名は、何でも良いのですが、ここでは AE2100 とします。
以上3つ設定したら、更新をクリックして初めの設定画面に戻ります。

トピック名はMQTTブローカー内でユニークである必要があります。
今回は ae2100/pressure とすればOKでしょう。
QoS1 にして、少なくとも1回はMQTTブローカーにデータが届くまで(複数回届く可能性があります)パブリッシュするようにします。

image.png

mqtt out の設定が完了したら、フローエディタの右上の デプロイ をクリックして保存・実行しましょう。
下図のように test.mosquitto.org ノードが 接続済 になったでしょうか?

image.png

ここでは 接続済 にならなくてもOKです。後ほど別の方法をご紹介します。
次の確認にすすみましょう。

フローの改造

RS485ノードと test.mosquitto.org ノードが接続済みになったので、1秒ごとにRS-485で入力される圧力データが10秒ごとにMQTTブローカーに送信されているはずです。

MQTTサブスクライブしてそのデータを受信してみましょう。
一度、MQTTブローカーにサブスクライブしておくと、以降パブリッシュがある度に、MQTTブローカーがパブリッシュされたデータを転送してくれます。

image.png

本来、サブスクライブは別クライアントで行うべきでしょうが、今回は Node-RED での説明を兼ねて、今まで使ってきたフローを2つに分けて、上の図のMQTTクライアント 1と2を Node-RED の1つのワークスペース上に実装します。

image.png

上のフローはセンサーデータを収集してクラウドにアップロードするエッジゲートウェイを想定しています。

RS485ノードからの圧力データはそのままダッシュボードに表示せず、MQTTブローカーにパブリッシュするだけにします。

そして下のフローは、エッジからのデータを受信してダッシュボード表示するWebアプリケーションを想定しています。
MQTTブローカーから転送された圧力データを受信してダッシュボードに表示します。

作業内容としては mqtt in ノードを新規に追加して、gaugechart ノードをこちらにつなぎ替えるだけです。

ダッシュボードのプロットが10秒に1回になれば、MQTTサブスクライブが確認できるはずです。

MQTTサブスクライブノード

それではmqtt in ノードを追加・設定しましょう。
パレットから下図のノードをワークスペースにおいて、ダブルクリックです。
image.png

サーバにはすでに test.mosquitto.org:1883 と入っているはずです。
(違う場合、ドロップダウンメニューで選択してください。)
トピック名にはパブリッシュ側の mqtt out ノードに設定した ae2100/pressure を入力します。
出力はデフォルトの自動判定のままにして、文字列として受信するようにします。
名前test.mosquitto.org とでもしておきましょう。

image.png

最後に、RS485ノードに接続している gaugechart のワイヤーを削除して、以下のように今追加した mqtt in ノードの右に置き、接続し直します。

image.png

以上で設定は終わりです。デプロイ をクリックして、保存・実行してみてください。
ダッシュボードに10秒に1回、圧力値がプロットされるはずです!

image.png

test.mosquitto.org に接続できない!

実はこの無料サーバーは時々つながらない事があります。
このサーバーはMQTTブローカーのリファレンス実装を公開している mosquitto.org により運用されているのですが、我々MQTTクライアント側がテストのために使用する目的だけでなく、MQTTブローカー自体をテストする目的でも使われており、不安定になったり、ダウンすることがあるそうです。

運悪くちょうどダウンしている場合、2,3日待ってトライしてみてください。
いや、今すぐ試してみたい!場合、このサービスはいかがでしょうか。
毎日パスワードを変更する必要がありますが、登録なしに無料で使えます。
もしくは node-red-contrib-mqtt-broker ライブラリを追加して、Node-RED上でMQTTブローカーを立ち上げる方法もあります。

さいごに

AE2100 によるローコードIoT開発、いかがだったでしょうか。
今回のサンプルアプリはローコードどころか、ノーコードでできてしまいました。

インタラクティブにアクセスする手段がSSHクライアントかXサーバー経由くらいしかないAE2100の使い方として、他のマシンでコンテナ内にアプリを作って、AE2100にそれをデプロイして実行するのが通常だと思います。

しかしNode-REDが動くと、AE2100で直接開発しても全く苦にならず、特に今回のシリアル通信のようなAE2100のハードウェアを使う場合、AE2100で直接開発することになるので、Node-RED は大きな助けになるはずです。

確かにNode-REDはメモリ食いだし、処理速度も速いとは言えないのですが、1秒くらいのレスポンスなら十分ではないでしょうか。
(まずNode-REDでぱぱっと作ってみて、処理速度が足りなさそうだったら、Node.jsやC言語で作り直す、というやり方もあるでしょう。)

これで4編にわたる記事もようやく終わりになります。
こんなに長くなるとは思わなかったのですが、AE2100 のユーザーのみなさんに少しでもお役に立てば幸いです。
最後の最後にお約束ですが、本記事に関してOKIは何も関知しないので、記事の内容に関してOKIに問い合わせることがないようにお願いいたします。

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

俺的RxSwiftまとめ④(disposebagとは?)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②,俺的RxSwiftまとめ③(Observableとは何か?)の続きです。

始め方はsubscribe/終わらせ方はdispose bag

Observableの特徴として、「3つのイベント(next/error/completed)を持ち、subscribeすることでそれらの要素を取り扱えるようになる」というものがあった。
始め方はsubscribeがあるが、終わらせ方はどのようにすれば良いのか?それは、下記の3通りが考えられる。

  • errorイベントが流れてくる
  • completedイベントが流れてくる
  • disposebag(ゴミ袋)にobservarbleを追加する

今回は、3番目のdispose bagについて詳しく記す。

disposebagとは何なのか?

observableをまとめて破棄するためのゴミ袋

なぜ必要なのか?

observableの中には、completederrorを流したくないものがある(ex:〇〇)。その場合は、observableに備わっているdisposeメソッドを用いて、明示的にobservableを完了させる(≒廃棄する)必要がある。(完了させないと、永遠にsubscribe状態になり、メモリリークの原因となってしまうため。)

その際、observable個別にdisposeメソッドを使う代わりに、管理コストを抑えるためにobservableをまとめて入れるdisposebagをインスタンスのプロパティとして持たせておいて、インスタンス破棄のタイミングで全てのobservabledisposeすることができる(ViewControllerのプロパティとして持たせるパターンをよく見かける)

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

Flutter系の記事のまとめ

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

俺的RxSwiftまとめ③(Observableとは何か?)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

俺的RxSwiftまとめ③(Observableとは何か? - その1)

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

俺的RxSwiftまとめ③(Observableとは何か? )

この記事は、俺的RxSwiftまとめ①,俺的RxSwiftまとめ②の続きです。

Observableとは何か?

observable,observable sequence,sequence,streamこれらは全部同じ意味を持つ言葉で、イベントの流れである。(RxSwiftでは、sequenceということが使われることが多い)

image.png
(引用)

Everything is a sequenceなのだ。

ここでいうイベントとは、数値や自身で定義したクラスのインスタンス、タップジェスチャーなどが含まれる。

このイベントは、以下のEnumで定義されている。

Event.swift
public enum Event<Element> {
    case next(Element)
    case error(Error)
    case completed
}

それぞれのcaseの場合どうなるか、Observable(sequence) 下記を表すマーブルダイアグラムとともに見ると、
image.png
nextの場合は、Observableは次のイベントを放出する。また、error/completedが呼ばれるまで、イベントを放出し続ける。

image.png
errorの場合はErrorを放出し、observableはイベントの放出をストップする。

image.png
completedの場合は、observableが停止し、イベントを放出をストップする。

どうやってObservableを用いて処理をするか?

Observableから放出されるイベントを使用するためには、subscribeする必要がある。(ObservableSubscribeされるまで、イベントを放出したり、operatorでの処理を行わない)
この際、イベントタイプごとに、handlerを追加することができる。
また、nextだったらElement,errorだったらエラーインスタンスをhandlerに渡すことができる。

下図のマーブルダイアグラムに示されたObservableについて考える

image.png

Example.swift
let one = "1"
let two = "2"
let three = "3"

let observable = Observable.of(one, two, three)
observable.subscribe { event in
  print(event)
}
出力
next(1)
next(2)
next(3)
completed

subscribeすると、イベントが順次扱えるようになる。
それをprintすると、出力のようにnextが3回流れ、最後にobservableの停止を意味するcompletedが流れてくる。

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

Core ML Toolsでレイヤーを編集する方法

Core ML Toolsを使ってモデルを変換していると、レイヤーを追加して間に計算処理を入れたり、レイヤーの一部をカットしたくなることがありますが、本稿はその方法を説明します。

Core ML Toolsは、ドキュメントも少なくネット上にも情報が少ないため、ソースコードを読みながら調べるしかないのですが、レイヤーを編集する作業はCore MLへの変換ではよくあるユースケースだと思いますので、この記事が役に立てばと思います。

モデルケース:出力の前にreshapeを入れて、出力のshapeを変更する

モデルケースとして、モデルの出力のshapeが(19)のものを、reshapeを追加して(1,1,19)にしてみます。

次元を追加しているだけなのでほとんど意味がないですが、単純なケースにしたいのでこうしています。

使用するモデルは、1桁の数字2つを与えると、それらを足した結果を予想(分類)するものを使います。分類結果は19個の配列になります。これは、1桁の数字を足すと0〜18で合計19種類の分類問題になるためです。

図にするとこんな感じです。

このモデルは以前の記事で使ったものと同じです。前回の記事は出力にラベル名まで与えていましたが、今回は単に19個の確率そのまま出力します。

Kerasで作った簡単なモデルをCoreMLを使ってiOSで動作させる

手順

それでは早速、Reshapeレイヤーを追加します。
Google Colaboratory上で作業をします。

なお、今回の作業の完全なコードはこちらにあります。
https://gist.github.com/TokyoYoshida/2fada34313385d63b666253490b5f3f4

1.Kerasのモデルを読み込む

Kerasで作ったモデルを使用します。モデルの作成部分は、前回の記事にあるので省略します。

notebook
from keras.models import load_model
keras_model = load_model('my_model.h5')

2.CoreMLに変換する

CoreMLに変換します。今回はラベルを貼らないのでシンプルにそのまま変換しています。

notebook
from coremltools.converters import keras as converter
mlmodel = converter.convert(keras_model)

3.ビルダーで読み込む

Core ML ToolsのNeuralNetworkBuilderに読み込みます。

notebook
import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)

モデルの情報を表示してみます。
ここから、モデルの出力はoutput1という名前で、shapeは19であることがわかります。

notebook
spec.description.output

# 出力結果
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

レイヤーの情報を表示します。

notebook
builder.layers
# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18']

今回の対象は、出力レイヤーであるactivation_18です。このレイヤーの出力に対してreshapeをかけます。

4.Rehapeレイヤーを追加する

NeuralNetworkBuilderにはadd_reshapeというメソッドがあり、reshapeレイヤーを追加できます。

ここで注意点なのですが、builderを使うと直感的にはもとのモデルを表すbuilderにそのままレイヤーを追加できるような気がしていまいますが、この方法だとうまくいきません。(2020/10/27追記、元のモデルを表すbuilderにレイヤーを追加できることがわかりましたので訂正致します)

例えば、先程builderに読み込んだモデルにそのままReshapeレイヤーを追加してみます。

notebook
reshape = builder.add_reshape(name='Reshape', input_name='activation_18', output_name='output', target_shape=(1,1,19), mode=0)

これをXcodeで読み込むとこんなエラーがでます。

Xcodeのエラー
There was a problem decoding this CoreML document
validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.

Python上でCoreMLの推論を実行しようとするとこんなエラーになります。

Pythonのエラー
RuntimeError: Error compiling model: "Error reading protobuf spec. validator error: Layer 'Reshape' consumes an input named 'activation_18' which is not present in this network.".

ネットで探してもなかなかわからず苦労したのですが、きっと同じことで苦労する人がいるのではと思います。
このエラーが出る理由は、input_name='activation_18'としているところです。ここには元のモデルでbuilder.spec.descriptionで表示したinputもしくはoutputの名前を指定する必要があります。

対処法ですが、NeuralNetworkBuilderを使って、レイヤーが1つだけの完結したモデルを作り、それを元のモデルに追加するようにします。
(2020/10/27修正)
add_reshapeの引数input_nameが元のモデルのoutput_nameと一致していれば、うまくレイヤーをつなぎこむことができます。

モデルにreshapeレイヤーを追加します。
inputは、元のモデルのoutput、つまりoutput1を指定します。これは新しいモデルの入力であるinput2のような気がしましたが、それだと元のモデルのoutputとreshapeレイヤーへの入力の紐付けがうまくいかないようです。
outputは、新しいモデルのoutputであるoutput2を指定します。
target_shapeは今回変換したいshapeである(1,1,19)を指定します。

notebook
builder.add_reshape(name='Reshape', input_name='output1', output_name='output2', target_shape=(1,1,19), mode=0)

これで、reshapeを追加したモデルができました。

レイヤー情報を確認します。

notebook
builder.layers

# 出力
# ['dense_4',
#  'dense_4__activation__',
#  'dense_5',
#  'dense_5__activation__',
#  'dense_6',
#  'activation_18',
#  'Reshape']

5.モデルの出力情報を変更する

モデルの最後にreshapeレイヤーがついたわけですが、それだけだとモデルの最後のレイヤーが差し替わっただけであり、モデル全体としての出力情報は変わっていません。

試しに出力情報を確認すると、期待したshapeである(1,1,19)ではなく(19)のままであることがわかります。

notebook
spec.description.output
# [name: "output1"
# type {
#   multiArrayType {
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

モデルの出力情報の変更は、これまたクセが強くてbuilder.spec.description.output[0]に対して代入しようとするとエラーになります。そのため、popしたりaddしたりしながら指定してきます。

notebook
// 出力情報を1つ削除する今回は出力情報は1つしかなかったので出力情報が消える
builder.spec.description.output.pop()
// 出力情報を1つ追加する
builder.spec.description.output.add()
// 出力情報の属性を指定する
output = builder.spec.description.output[0]
output.name = "output2"
output.type.multiArrayType.dataType = coremltools.proto.FeatureTypes_pb2.ArrayFeatureType.ArrayDataType.Value('DOUBLE')
// shape情報として(1,1,19)を設定する
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(1)
output.type.multiArrayType.shape.append(19)

(補足)
上の例では、pop()で消してadd()で追加することでクリーンな状態にしてから情報を追加していましたが、もともと存在するoutputの属性値だけを直接書き換えることはできます。

例えば、shapeを書き換えるのは次のようにします。

shapeを書き換える例
builder.spec.description.output[0].type.multiArrayType.shape[0] = 100

(補足終わり)

出力情報を確認すると、うまく設定できていることがわかります。

notebook
builder.spec.description.output
# [name: "output2"
# type {
#   multiArrayType {
#     shape: 1
#     shape: 1
#     shape: 19
#     dataType: DOUBLE
#   }
# }
# ]

6.モデルの出力結果をJupyter Notebookで確認する

モデルの出力結果をMac上で動作しているJupyter Notebookで確認します。
なぜMacでJupyter Notebook?という話なんですが、Google ColaboratoryはバックエンドがLinuxなのでCoreMLの推論はできず、モデルが正しく動作しているかの確認ができません。

Xcodeで推論してもよいのですが、Xcodeはおかしなモデルを読むとエラーを出してくれることもありますが、ビルド時に次のエラーを吐いていきなり落ちることもあります。

Xcodeのエラー
Command CoreMLModelCompile failed with a nonzero exit code

こうしたエラーが出たときは、Jupyter Notebookで実行することでエラーの詳細の情報を確認すると良いです。

ということで、Jupyter Notebookを起動します。

こちらに完全なコードがあります。
https://gist.github.com/TokyoYoshida/632b4c8070aa6c937539e4ae261a2740

モデルへの入力として[2,3]を与えて推論を実行してみます。

JupyterNoteBook
coreml_model_path= "my_model_with_builder.mlmodel"

import coremltools
spec = coremltools.utils.load_spec(coreml_model_path)
builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)

mlmodel = coremltools.models.MLModel(spec)

mlmodel.predict({'input1': np.array([2.0,3.0])})

# {'output2': array([[[2.56040733e-21, 7.25779588e-15, 1.99342376e-10, 1.11184195e-09,
#           5.92091055e-05, 9.99939799e-01, 9.72097268e-07, 1.13292452e-14,
#           4.43997455e-23, 5.00404492e-33, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
#           0.00000000e+00, 0.00000000e+00, 0.00000000e+00]]])}

今回のモデルは2つの要素を足し算してくれるモデルなので、5番目の要素の確率がほぼ1になっていることから正しく推論できていることがわかります。

また、出力されたarrayが[[[確率の入った19個の配列]]]というとなっていることから、shapeが(1,1,19)と意図通りになっていることが確認できます。

7.モデルの出力結果をXcode & アプリで確認する

最後に、モデルの出力結果をXcodeに載せてアプリで確認します。

完全なコードはこちらです。
https://github.com/TokyoYoshida/CoreMLSimpleTest

ViewController.swift
let model = my_model_with_builder()
let inputArray = try! MLMultiArray([2,3])
let inputToModel = my_model_with_builderInput(input1: inputArray)
if let prediction = try? model.prediction(input: inputToModel) {
print(prediction.output2)
try! print(prediction.output2.reshaped(to: [19]))
}

// # Double 1 x 1 x 19 array
// # Double 19 vector
// # [2.422891379885771e-21,7.01752566646674e-15,1.959301054732521e-10,1.089580203839091e-09,5.933549255132675e-05,0.9999396800994873,9.530076567898504e-07,1.087061586308846e-14,4.16250629238845e-23,4.617410135639087e-33,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,1.401298464324817e-45,0,0,0]
// 

こちらも意図通りになっていることが確認できます。
確率の数字が微妙に違うかもしれませんが、これはモデルを訓練しなおしたためなので気にしないで下さい。

番外編 レイヤーを削除する方法

動作確認はしていませんが、レイヤーを削除する方法も書いておきます。

レイヤーを削除する。

notebook
layers = builder.spec.neuralNetwork.layers
# レイヤーの後ろから2番めを取得(activation_18)
item = layers[-2]
# その要素を削除
layers.remove(item)

レイヤーの情報を表示すると、削除されていることがわかります。

notebook
for layer in layers:
  print(layer.name)
# dense_4
# dense_4__activation__
# dense_5
# dense_5__activation__
# dense_6
# Reshape

最後に

NoteではiOS開発について定期的に発信していますので、フォローしていただけますと幸いです。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem

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

【Swift 】通知を出す方法について

通知を許可しているか確認

通知を出す場合、事前に通知を許可しているか確認する必要があります。

// 通知許可の取得
UNUserNotificationCenter.current().requestAuthorization(
     options: [.alert, .sound, .badge]){
     (granted, _) in
     if granted{
          UNUserNotificationCenter.current().delegate = self
     }
}

指定時間経過で通知を出す

○秒後に実行させる場合、UNTimeIntervalNotificationTrigger(timeInterval: , repeats:)で通知を作成させる必要があります。

let content = UNMutableNotificationContent()
content.sound = UNNotificationSound.default
content.title = "タイトル"
content.subtitle = "サブタイトル"
content.body = "内容"

// 指定時間後に実行
let timer = 10

// 通知リクエストを作成
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(timer), repeats: false)
let identifier = NSUUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

// 通知リクエストを登録
UNUserNotificationCenter.current().add(request){ (error : Error?) in
     if let error = error {
          print(error.localizedDescription)
     }
}

指定時刻になったときに通知を出す

指定時刻に実行させる場合、UNCalendarNotificationTrigger(dateMatching: , repeats:)で通知を作成させる必要があります。

let content = UNMutableNotificationContent()
content.sound = UNNotificationSound.default
content.title = "タイトル"
content.subtitle = "サブタイトル"
content.body = "内容"

// 通知時刻を指定
let date = Date()
let newDate = Date(timeInterval: 60, since: date)
let component = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: newDate)

// リクエストを作成
let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false)
let identifier = NSUUID().uuidString
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

// 通知リクエストを登録
UNUserNotificationCenter.current().add(request){ (error : Error?) in
     if let error = error {
          print(error.localizedDescription)
     }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VisionKitの文書スキャナーは、コンビニのマルチコピー機のスキャナーの代わりに使えるか

iPhoneのカメラで紙の文書をスキャンして、普通のスキャナーの代わりにネット上の書類のやりとりに使えるのでしょうか? 試してみました。

AppleのVisionKitを使えば、文書をスキャンできます。
斜めに撮っても、コンピュータービジョンでまっすぐな文書に直してくれます。

Oct-25-2020 03-19-11.gif

つかいかた

import VisionKit
let documentCameraViewController = VNDocumentCameraViewController()
documentCameraViewController.delegate = self
present(documentCameraViewController, animated: true)

ドキュメントスキャナーにプリセットされているSaveボタンで写真ライブラリに保存するコードはこちら。
Info.PlistのPrivacy Camera Additional Usage Descriptionを設定しておくことを忘れずに。

// Saveボタンが押された時に呼ばれるデリゲートメソッド
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    let image = scan.imageOfPage(at: 0)
     UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

コンビニのスキャナーとの比較

1、VisionKit

IMG_2736.png

2、コンビニのスキャナー

scan-001のコピー.png

コンビニの方が圧倒的に綺麗でした。

Visionで文字読み取りする前に紙から真っ直ぐにして取り込むのとかには使えそうです。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

iPhone(VisionKit)の文書スキャナーは、コンビニのマルチコピー機のスキャナーの代わりに使えるか

iPhoneのカメラで紙の文書をスキャンして、コピー機のスキャナーの代わりに書類のやりとりに使えるのでしょうか? 試してみました。

AppleのVisionKitを使えば、文書をスキャンできます。
斜めに撮っても、コンピュータービジョンでまっすぐな文書に直してくれます。

Oct-25-2020 03-19-11.gif

つかいかた

import VisionKit
let documentCameraViewController = VNDocumentCameraViewController()
documentCameraViewController.delegate = self
present(documentCameraViewController, animated: true)

ドキュメントスキャナーにプリセットされているSaveボタンで写真ライブラリに保存するコードはこちら。
Info.PlistのPrivacy Camera Additional Usage Descriptionを設定しておくことを忘れずに。

// Saveボタンが押された時に呼ばれるデリゲートメソッド
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    let image = scan.imageOfPage(at: 0)
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

コンビニのスキャナーとの比較

1、VisionKit

IMG_2736.png

2、コンビニのスキャナー

scan-001のコピー.png

コンビニの方が圧倒的に綺麗でした。
蓋をして光を当てているので、シワが全然ない。

VisionKitは、Visionで文字認識する前に、紙から取り込むのとかには使えそうです。

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

iOSアプリ開発:タイマーアプリ(9.プログレスバーの色をカスタマイズする)

スクリーンショット 2020-10-28 11.20.58.png

記事

タイマーアプリを作成するためのポイントを複数記事に分けて掲載しています。
この記事では、プログレスバーに自作のグラデーションカラーを適用し、時間経過とともに色相を変化させる方法について掲載します。

環境

  • OS: macOS 10.15.7 (Catalina)
  • エディタ: Xcode 12.1
  • 言語: Swift
  • 主な使用ライブラリ: SwiftUI

Gitリポジトリ

以下のGitリポジトリのURLからサンプルコードをご覧いただけます。
https://github.com/msnsk/Qiita_Timer.git

手順

  1. プログレスバーをどういう色にするか検討する
  2. ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する
  3. ProgressBarView にグラデーションを生成するメソッドを作成する
  4. プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する
  5. ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる

1. プログレスバーをどういう色にするか検討する

プログレスバーの色を美しく表現するにはどうすれば良いかを考えます。これは人によって好みが別れるかもしれません。私の場合、は以下の2点が重要だと考えました。
一つは、単色ではなくグラデーションで表示すること。
もう一つは、時間経過とともに色が変化すること。
この2つを実装して、アプリのビジュアル的な印象を向上させます。

2. ProgressBarView に変化する色相(Hue)の数値を格納するプロパティを作成する

グラデーションを作るには最低 2 色が必要です。この2色を自作していきます。

Swift では、色の指定方法はいくつかあります。それは一般的な色の規格とも合致しています。例えば、RGB、CMYK、HSB などです。

簡単に説明すると、RGB は Red、Green、Blue の光の三原色をそれぞれどの程度配合するかで色を表現するものです。3 色全て 100% なら白、 0% なら黒になります。

CMYK は色の三原色 + K:Black で、RGB とは逆に 3 色全て 100% なら黒、0% なら白になります。インクジェットプリンタを想像するとわかりやすいかと思います。

そして、HSB は、色相(Hue)、彩度(Saturation)、明度(Brightness)で色を表します。Hue は何色かを示し、Saturation は色味がどの程度か(0ならモノトーン)、Brightness は白の割合で 100% なら白なので色が濁りません。

最終的に色を常に変化させることを考えると、HSB ならば、Hue の値のみを変化させ、Saturation と Brightness を固定しておけば調整しやすそうですので、ここでは HSB での色指定を採用します。

それでは ProgressBarView に色相(Hue)を格納するプロパティを2色分追加します。それぞれ customHueA、customHueB とします。

HSB での色指定の場合、Color() は以下のような引数が求められます。それぞれデータ型は CGFloat 型で、最小値が 0.0、最大値が 1.0 と定められています。

Color(hue:, saturation:, brightness:)

そのため、新規に追加する Hue のプロパティの値の初期値として、ひとまず 0.5 と 0.3 をそれぞれ代入しておきます。0.5 が青緑色、 0.3 が黄緑色で、比較的近い色味にしておきます。

例えば 0.0 と 0.5 だと色相が真逆になって最終的にグラデーションにしたときに中間が濁った色になりがちです。そのため、ここでは色相の差を 0.2 にとどめておきました。ちなみに Hue 0.0 と Hue 1.0 では一周回って同じ色になります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 0.5
    @State var costomHueB = 0.3

    var body: some View {
        //(省略)
    }
}

3. ProgressBarView にグラデーションを生成するメソッドを作成する

次に、Hue の値を格納した2つのプロパティから、円形プログレスバーにふさわしいグラデーションを作成するメソッドを作成していきます。

メソッドの名前は makeGradientColor としました。

引数は Double 型のデータをとる引数 hueA と hueB です。ここに先に作成したプロパティを入れる予定です。

そして、戻り値のデータ型は、円すい状グラデーションの AngularGradient です。ちなみに、Swift のグラデーションは他にも、線形グラデーション(LinearGradient)や放射状グラデーション(RadialGradient)があります。円をぐるりと一周する方向でグラデーションがかかるのが AngularGradient です。

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {}

次に makeGradientColor メソッドの {} の中を記述していきます。まず、メソッドの {} 内の最初にグラデーションに必要な2色を作るプロパティを用意します。

ColorA は引数 hueA の値から色を作ります。ここには ProgressBarView のプロパティ costomHueA が入る予定です。同様に、ColorB は引数 hueB の値から色を作ります。こちらには costomHueB が入る予定です。

ColorA も ColorB も saturation と brightness は同じ数値を入れます。再度はやや高め、明度はかなり高めにしています。

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
}

そして最後に、作った2色から円錐型グラデーションを作ります。AngularGradient 構造体のインスタンスを作成するわけですが、引数 gradient には イニシャライザ .init() を入れ、さらにその引数には Array でグラデーションの開始から終了までの色を入れます。

ここで以下のようにすると、円の始点と終端の重なるところで、くっきり色が分かれてしまいます。

[colorA, colorB]

ですので、以下のように、colorB からまた colorA へグラデーションをかけるようにします。

[colorA, colorB, colorA]

もっと細かく中間にも色を置いて調整することもできます。例えば以下のような具合です。ここでは2色でのグラデーションにとどめておきます。

[colorA, colorB, colorC, colorD, colorA]

func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
return gradient
}

メソッドは body{} の下に記述します。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 1.0
    @State var costomHueB = 0.5

    var body: some View {
        //(省略)
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}

4. プログレスバーの Circle() の .stroke モディファイアの色の引数にメソッドを追加する

これまでプログレスバーの円の色は .stroke モディファイアの引数で単色のシアンを指定していました。

.stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))

ここで、さきほど作ったメソッドを Color(.cyan) の代わりに引数として入れます。メソッドの引数 hueA と hueB には先に用意していたプロパティ costomHueA と costomHueB をそれぞれ入れます。

.stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))

コード全体としては以下のようになります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 1.0
    @State var costomHueB = 0.5

    var body: some View {
        ZStack {
            //(背景用の円省略)

            //プログレスバー用の円
            Circle()
                .trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
                //メソッドで生成したグラデーションカラーを適用
                .stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(15)
        }
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}

5. ProgressBarView に .onReceive モディファイアを追加し、 プログレスバーの色を常に変化させる

手順4で、プログレスバーの色をプロパティの色相から指定する形でグラデーション表示にすることができました。

さらに、onReceive モディファイアで、TimeManager クラスの timer プロパティ(Timer.publish() メソッド)の発動をトリガーにして、毎 0.05 秒ごとに色相のプロパティ costomHueA と costomHueB の値を変化させて、プログレスバーの色が常に変化するようにしていきます。

0.05 秒ごとに色相の値を 0.005 ずつ足していきます。もっとゆったり色を変化させたい場合は 0.001、もっとコロコロ色が変わる方が良ければ 0.01 くらいが良いかと思います。これ以上数値を上げると、色がただチカチカして目が痛くなりそうな動きになってしまいます。

また、色相は1.0が最大値ですので、1.0になったら、0.0に戻すように if 文で記述します。

.onReceive(timeManager.timer) { _ in
    self.costomHueA += 0.005
    if self.costomHueA >= 1.0 {
        self.costomHueA = 0.0
    }

これを costomHueB の分も同様に記述します。コード全体では以下のようになります。

ProgressBarView
struct ProgressBarView: View {
    @EnvironmentObject var timeManager: TimeManager

    @State var costomHueA = 0.5
    @State var costomHueB = 0.3

    var body: some View {
        ZStack {
            //背景用の円
            Circle()
                .stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
                .scaledToFit()
                .padding(15)

            //プログレスバー用の円
            Circle()
                .trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
                //メソッドで生成したグラデーションカラーを適用
                .stroke(self.makeGradientColor(hueA: costomHueA, hueB: costomHueB), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                .scaledToFit()
                //輪郭線の開始位置を12時の方向にする
                .rotationEffect(Angle(degrees: -90))
                .padding(15)
        }
        //毎0.05秒ごとに発動
        .onReceive(timeManager.timer) { _ in
            self.costomHueA += 0.005
            if self.costomHueA >= 1.0 {
                self.costomHueA = 0.0
            }
            self.costomHueB += 0.005
            if self.costomHueB >= 1.0 {
                self.costomHueB = 0.0
            }
        }
    }

    func makeGradientColor(hueA: Double, hueB: Double) -> AngularGradient {
        let colorA = Color(hue: hueA, saturation: 0.75, brightness: 0.9)
        let colorB = Color(hue: hueB, saturation: 0.75, brightness: 0.9)
        let gradient = AngularGradient(gradient: .init(colors: [colorA, colorB, colorA]), center: .center, startAngle: .zero, endAngle: .init(degrees: 360))
        return gradient
    }
}

これで美しいグラデーションで、かつ常に色相が変化する円形プログレスバーができました。

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