PyTorchでマスクしている人を分類してみた

これまでPythonをメインの言語としてプライベートで開発をしていましたが、だいたいWebアプリやIoT系の開発がメインになってしまい、機械学習やDeepLearningは最初のチュートリアルをちょっと触った程度で終わっていました。 Pythonやっててそれはさすがにまずいと思い、自分でDeeplearningで何か実装してみようと思っていました。 そこで今回はマスクを付けている人をPyTorchで分類するモデルを作ってみようと思います。

データセット

まず肝心なデータセットの用意をします。 それぞれ以下のサイトから入手しました。

cabani/MaskedFace-Net - マスクをつけた顔画像のデータセットを取得

Real and Fake Face Detection - マスクをつけていない顔画像のデータセットを取得(Real のデータを使用)

Datasetクラスを作る

PyTorchでは自前のデータセットを扱うにはDatasetクラスを用意する必要がありますが、用意したデータセットを以下のディレクトリ構成で保存すれば、PyTorchのImageFolderを使うことができます。 dataディレクトリを参照することで各クラスのデータセットを取り込みます。

data
├─without_mask
└─with_mask

データセットTensor型に変換するtransformを合わせると以下のようにDatasetクラスを定義できます。

from torchvision import transforms
from torchvision.datasets import ImageFolder
# データセットの読み込み
data_transforms = {
    'train': transforms.Compose([
        transforms.Resize(size=(224,224)),
        transforms.RandomRotation(degrees=15),
        transforms.ColorJitter(),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(size=(224,224)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

dataset = ImageFolder("data", data_transforms['train'])
dataset.class_to_idx

dataset.class_to_idxでクラス名、IDの一覧を辞書で確認できます。 以下のようにクラス名、IDが割り振られていたらOKです。

{'with_mask': 0, 'without_mask': 1}

読み込んだデータを訓練用、評価用でそれぞれ分けます。 (訓練用8割、評価用2割)

# 訓練用、評価用にデータを分ける
all_size = len(dataset)
train_size = int(0.8 * all_size)
val_size = all_size - train_size
dataset_size  = {"train":train_size, "val":val_size}
train_data, val_data = random_split(dataset, [train_size, val_size])

DataLoaderの定義とデータの中身の確認

次にDataLoaderを定義します。

# DataLoaderを定義
batch_size = 60
train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False)
dataloaders = {'train':train_loader, 'val':val_loader}

ここで読み込んだデータの中身を確認します。 ただデータ量が多いので、torchvision.utils.make_grid(images, nrow=10)を使って表示数を絞り込みます。 データの中身を見るときにはmatplotlibをつかいます。

dataiter = iter(dataloaders['train'])
images, labels = dataiter.next()

# Make a grid from batch
out = torchvision.utils.make_grid(images, nrow=10)
plt.imshow(out.permute(1, 2, 0))

実行するとデータセットの中から無作為に画像データを選び、その画像を出力します。

f:id:KMiura:20210118024825p:plain

モデルの読み込み

torch.hub.loadを使うとトレーニング済みのモデルを読み込むことができます。 今回はこれを使ってMobileNetV2を読み込みたいと思います。

Model = torch.hub.load('pytorch/vision:v0.6.0', 'mobilenet_v2', pretrained=True)

転移学習

このモデルはすでに既存のデータで学習しているため、今回用意したデータを学習させるためには転移学習を行います。 以下のようにパラメータを取り出して、各層をフリーズさせます。

for param in Model.parameters():
    param.requires_grad = False

ここで今回用意したデータを学習させるために、最終層のネットワークを上書きします。

# 今回の学習用に最終層のネットワークを上書き
Model.classifier[1] = nn.Sequential(
                      nn.Linear(1280, 256),
                      nn.ReLU(),
                      nn.Linear(256, 128),
                      nn.ReLU(), 
                      nn.Dropout(0.4),
                      nn.Linear(128, 64),
                      nn.ReLU(),
                      nn.Linear(64, 32),
                      nn.ReLU(),
                      nn.Dropout(0.4),
                      nn.Linear(32, 2), 
                      nn.LogSoftmax(dim=1))

ロス関数とoptimizerを定義

cross-emtropyとSGDを定義します。

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

レーニン

用意したネットワークとotimizerを使ってトレーニングを行います。

for epoch in range(EPOCH):
    for phase in ['train', 'val']:
        if phase == 'train':
            model.train()
        else:
            model.eval()

        running_loss = 0.0
        running_corrects = 0

        for inputs, labels in dataloaders[phase]:
            inputs = inputs.to(device)
            labels = labels.to(device)
            
            with torch.set_grad_enabled(phase == 'train'):
                outputs = model(inputs)
                _, preds = torch.max(outputs, 1)
                loss = criterion(outputs, labels)

                # backward + optimize only if in training phase
                if phase == 'train':
                    optimizer.zero_grad()
                    loss.backward()
                    optimizer.step()
            running_loss += loss.item() * inputs.size(0)
            correct = torch.eq(torch.max(F.softmax(outputs, dim=1), dim=1)[1], labels).view(-1)
            running_corrects += torch.sum(correct).item()
        epoch_loss = running_loss / dataset_size[phase]
        epoch_acc = running_corrects / dataset_size[phase]
        loss_dict[phase].append(epoch_loss)
        acc_dict[phase].append(epoch_acc)
        
        print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
        
        if phase == 'val' and epoch_loss<0.005:
            best_loss = epoch_loss
            best_acc = epoch_acc
            best_model_wts = copy.deepcopy(model.state_dict())

    if (epoch+1)%5 == 0:#5回に1回エポックを表示
        print('Epoch {}/{}'.format(epoch+1, EPOCH))
        print('-' * 10)

モデルの保存

レーニングが終わったモデルを保存します。 Pytorchで保存したモデルを呼び出すようにしたいときには以下の1行で完了します。

torch.save(net.state_dict(),"detector/face_mask_detector.pth")

失敗談

ここで素人ならでは(?)の失敗談をまとめてみます。

学習結果が発散してる

f:id:KMiura:20210119032520p:plain

最初に学習をかけたときにどういうわけか学習結果が波打つように変化したグラフが出力されました。 なんかの信号入力をグラフにしたのかと思うぐらいですw。 これは逆伝搬したときに勾配が更新されることが原因のようです。 これによって、すでに存在している勾配があるとそこに勝手に追加されてしまうため、lossのグラフが発散してしまったようです。 そこでトレーニングの段階でパラメータの更新が終わったらoptimizer.zero_grad()を行うことで勾配情報がリセットされて前のトレーニングの結果に引きずられることがなくなります。

↓こちらの記事が参考になりました

ohke.hateblo.jp

中途半端な学習結果

f:id:KMiura:20210119034024p:plain optimizer.zero_grad()を入れてなんとか理想に近いグラフはできましたが、ノイズも混ざって中途半端なグラフになりました。 Validに関しては1に到達しているものもあります(1に到達せず収束するのが理想)。 これはデータ数を増やせば解決します。 各クラス1600枚ほど用意するとこんな感じで激しいノイズを起こさずにきれいに収束したグラフをできました。 f:id:KMiura:20210119034939p:plain

ソースコード

今回作ったソースコードはこちらのGithubレポジトリで公開しています。

github.com

まとめ

今回は今までチュートリアル止まりで終わっていたディープラーニングを自分で実装してみました。 難しそうに見えたものもライブラリの力で簡単に実装できるのはいいなと思いました。 特にPytorchは導入もシンプルでドキュメントも割と丁寧なのでこれからどんどん使いこなしたいですね。

Node-REDでTelloを動かすためのノードを作って公開してみた

この記事は個人開発 Advent Calendar 202013日目の記事です。

これまでNode-REDを使ってきましたが、どこか物足りなさを感じていました。

それは自分で自作ノードを作ってみたいということです。

最近ではいろんな人達がノードを公開しているので、探せばやりたいことを実現できると思ってそこで止まっていました。

それで話変わりまして、数ヶ月前にTelloを衝動買いして早速触ったことも無いScratchを使ってTelloを制御してみました。

これを動かしていたときにいろいろ思うことがありました。

  • Scratch動かしてるけど実際はローカルで立てたNode.jsにコマンドをHTTPリクエストしてる→準備がめんどくさい
  • Scratch2.0で動かす必要がある→Macでは起動自体めんどくさい
  • 同じNode.jsならNode-REDで動かしたほうが楽そう(小並感)

そこで、TelloをNode-REDで動かす事例を調べてみるとデフォルトのUDPノードを使った事例が紹介されていました。

flxy.jp

ありものを使うのもいいですが、やはりScratchのときのような手軽さを追求したいという気持ちが湧いてきました。

というわけでTelloをNode-REDで簡単に操作するためのノードを作ってみようと思いました。

要件定義

特に明確にしたわけではありませんが、Node-REDで使えるようにするなら外せない条件をリストアップしてみました。

  • Scratch版で提供している基本動作(離陸、着陸、上下左右など)は最低限実装する
  • 実行結果をmsg.payloadで出力する
  • ステータス(バッテリー残量、温度など)を出力する機能も実装する

これだけ入れておけば、ただのScratchを移植しただけではない、Node-REDを生かしたものになるんじゃないかなと思います。

公開したノード

node-red-contrib-telloという名前で公開しています。

READMEは英語ですが、Node-REDを使ったことある方なら多分雰囲気で動かせると思います。

このブログでも実装面の解説をしているので、なんとなく使い方が分かると思います

flows.nodered.org

実装

細かい実装については、以下のGithubレポジトリを眺めてもらうといいですが、ここではポイントを絞ってお話します。

github.com

基本動作

このノードではすべて裏側でUDP送信を行うsendCommand関数を用意しています。

この辺はScratchのノードを動かすときに使っていたNode.jsのコードを参考にしています。

離陸と着陸

これはべつに値が変化することでも無いので、それぞれつなげるだけ実行するようにしてみました。

値の受け取り(数値)

距離や角度を指定するノードではノードの設定で数値を指定することはできますが、入力欄を空欄にするとmsg.payloadで受け取った値をコマンドの入力として使います。

こうすることで例えばセンサーの値を受け取ってそれをTelloに反映するということをやることもできます。(そのうちサンプルを用意します)

f:id:KMiura:20201213230443p:plain

値の受け取り(セレクトボックス)

セレクトボックスを使ったノード(flipノードなど)はHTMLのセレクトボックスを使っているので値はvalue属性で担保しているので、Scratchのときよりもメニューが見やすくなったと思います。

ちなみにセレクトボックスの値は必ずどれか選択するようにしています。(これをmsg.payloadで受け取れるようにしたら、ノードのゲシュタルト崩壊してしまう…)

f:id:KMiura:20201213232536p:plain

stateノード

stateノード以外は挙動がほぼ同じなので、基本的にはフロントのHTMLを修正したりコマンドを送信する前の処理の修正してあとはコピペするだけでそんなに時間はかかりません。

ところが、このstateノードについては一番実装に苦戦しました。

というのもUDPはHTTPリクエストと違って受信と送信は一方通行になっています。

そのためUDPで受け取った値はグローバル変数に格納しています。

そのときにstateコマンド(ケツに?を入れるコマンド)を実行するとその返答はコマンド送信を完了したときに受け取るOKの後に連続でstateコマンドで聞いた値を返すようになっています。

これをどうやってノードを使って返そうか悩みましたが、単純にPromiseを使って時間差でコードを実行するようにして最終的にstateコマンドの結果を取れるようにしてみました。

JavaScriptって非同期のせいかスリープの処理にもクセがあって毎回実装辛いですね…

node.on("input", function (msg) {
      Promise.resolve()
        .then(function () {
          sendCommand("command");
        })
        .then(function () {
          return new Promise(function (resolve, reject) {
            setTimeout(function () {
              sendCommand(node.command);
              resolve();
            }, 500);
          });
        })
        .then(function () {
          return new Promise(function (resolve, reject) {
            setTimeout(function () {
              RED.log.info("Result state command: " + telloState);
              msg.payload = telloState;
              node.send(msg);
              resolve();
            }, 500);
          });
        });
    });

だいぶ力技ですが、これでちゃんと値が取れるようになりました。

トラブルシューティング

  • レスポンスが出力されず、文字列が空
    • Telloの動作が不安定な可能性があります。Telloを再起動するか、バッテリーをフル充電しましょう
  • ノードに値を入れたのに動作しない
    • No invalid imuと出る場合はimuセンサーが機能していないことがあります。Telloを再起動かバッテリーをフル充電しましょう。
    • 本体を触って熱かったら電源を落として冷めるまで待ちましょう。
  • そもそもtakeoffしない
    • Node-REDがTelloと接続できていない可能性があります。TelloとPCの接続を確認して問題なさそうでしたら、Node-REDを再起動して試してみましょう。

まとめ

今回は僕が初めてリリースしたNode-REDライブラリの話をしました。

最初は難しそうだったので、なかなか作る機会がなかったのですが、多少JavaScriptがわかっていればそんなに実装に困ることはなくて意外と簡単にできました。

改善点がありましたらレポジトリへのissueやPR大歓迎です!

Node-REDのobnizノードを使ってobnizを動かしてみた

Node-RED Con Tokyo 2020に参加してきました。

nodered.jp

昨年も開催されたNode-RED UG Japan主催のカンファレンスで中身の濃い知見を得られました。

この中のobnizのセッションの中で、カンファレンスの前日にリリースしたばかりだというobnizノードの紹介がありました。

その昔、ハッカソンでNode-REDを使ってobnizから送られてくるセンサの値を受け取るということに挑戦したのですが、開発にかなり苦戦した覚えがあります。

おまけに初めてobniz触ったこともあり、いまいち使い勝手が分からなかったんですよねw。

そんなobnizをNode-REDから操作できるノードが公式でリリースされたのはいいですね。

というわけで今回はこのobnizノードを使ってIBM CloudのNode-REDからobnizを操作してみたいと思います。

ちなみにNode-REDの環境を用意できれば動作環境は何でもいいと思います。 環境構築方法はNode-RED UG Japanのドキュメントが参考になります。

nodered.jp

インストール

まずはインストールしてみます。インストール方法はここにあったので、この通りにやってみます。

flows.nodered.org

Node-REDを立ち上げたら右上のメニューから「パレットの管理」を選択します

f:id:KMiura:20201010205616p:plain

「ノードを選択」タブを選択して検索窓にobnizと検索すれば「node-red-contrib-obniz」が1件ヒットすると思うので、「ノードを追加」をクリックして、インストールします。

f:id:KMiura:20201010210129p:plain

途中でこんなダイアログが出ますが、気にせず「追加」をクリックします。

f:id:KMiura:20201010210634p:plain

パレットの管理画面を閉じて、左側のノードの一覧にこのようにノードが出てきたらインストールは完了です。

f:id:KMiura:20201011012855p:plain

とりあえず動かしてみた

とりあえずobniz のディスプレイに何か文字を表示するフローを作ってみます。

フローはこんな感じです。injectノードから適当な文字列を送ると、obnizのディスプレイにその文字が表示されます。

f:id:KMiura:20201010211230p:plain

ここでよく見るとobnizノードの右上に赤い三角がついてますね。

obnizノードを使うにはお使いのobnizのデバイス情報を登録する必要があります。

というわけで接続したobnizのfunctionノードをダブルクリックしてノードの編集画面を開きます。 「新規にobnizを追加」の横にある鉛筆マークをクリックして、obnizの接続情報を追加しましょう。

f:id:KMiura:20201010211844p:plain

プロパティの画面では色々設定する必要があります。設定項目としては以下の通りです。

項目 詳細
obniz ID obnizをWifiに接続すると表示するID
access token obnizの開発者コンソールで取得するトークン (設定していない場合は空欄でOK)
device type バイスの種類

一通り設定すると、以下のようになります。僕はアクセストークンを設定していないのでaccess tokenは空欄になっています。

f:id:KMiura:20201010220427p:plain

これで「更新」をクリックするとノードの編集画面に戻ります。

続いて「コード」の欄にデバイス操作を行うためのコードを書きます。

コードについては公式ドキュメントのコードを書くと操作できます。

obniz.com

これを元にコードには、以下の2行を書きます。

obniz.display.clear();
obniz.display.print(msg.payload);

1行目のobniz.display.clear();をやらないとディスプレイの内容が上書きされて表示がぐちゃぐちゃになるので入れておいたほうがいいです。

2行目のobniz.display.print();の引数で指定した文字列をディスプレイに表示させます。

今回はinjectノードからの入力を表示するため、msg.payloadを引数にしています。

コードの記入を終えたら「完了」を押して、編集を終えます。

f:id:KMiura:20201010221727p:plain

あとは右上の「デプロイ」ボタンをクリックしてデプロイして、以下のようにconnectedが表示されればobnizとNode-REDは正常に接続されています。

f:id:KMiura:20201010222143p:plain

injectノードの左側のボタンをクリックして、接続されたobnizのディスプレイに文字列が表示されたら成功です。

f:id:KMiura:20201010222803j:plain

入力もやってみる

せっかくなので、もう片方のrepeatノードの方も試してみます。

このノードはobnizから何かしらのインプットがあったときに使えるノードです。

ここでは、obnizの上についているスイッチで何か操作すると、デバッグにその操作内容が表示するというフローを作ってみます。

フローとしては以下の通りです。

f:id:KMiura:20201010225619p:plain

まずは、obniz repeatノードをダブルクリックして編集画面を開きます。先程設定したobnizのデバイスが選択されていたらデバイスの設定は不要です。

もし何も表示されていない場合は、セレクトボックスから先程登録したデバイスを選択します。

このノードは一定の間隔でコードを実行する仕組みなので、以下のコードをコード欄に貼り付けます。

var state = await obniz.switch.getWait();
msg.payload = state;

return msg;

こちらもNode-REDで標準で使えるfunctionノードの使い方と同じになるので、実行結果を出力するには、最後に return msg;を書く必要があります。

次のswitchノードは何も入力が無かったときにnoneが出力されるので、none以外の出力を表示するために条件分岐を入れています。

f:id:KMiura:20201010231512p:plain

フローを作成してデプロイを行うと、以下のようにobinizのスイッチの状態(none以外)がデバッグ画面に表示されます。

f:id:KMiura:20201011005013p:plain

今回のフロー

[{"id":"523da830.10ea08","type":"tab","label":"obniz Control","disabled":false,"info":""},{"id":"fcbde703.c35d28","type":"obniz-function","z":"523da830.10ea08","obniz":"a8808e2f.d98e9","name":"","code":"obniz.display.clear();\nobniz.display.print(msg.payload);","x":560,"y":320,"wires":[["b1dcc84e.b9b438"]]},{"id":"bb4f92aa.2cab","type":"inject","z":"523da830.10ea08","name":"","topic":"","payload":"Hello World!","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":270,"y":300,"wires":[["fcbde703.c35d28"]]},{"id":"885dcb9f.d6ddf8","type":"inject","z":"523da830.10ea08","name":"","topic":"","payload":"Goodbye","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":260,"y":360,"wires":[["fcbde703.c35d28"]]},{"id":"b1dcc84e.b9b438","type":"debug","z":"523da830.10ea08","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":820,"y":320,"wires":[]},{"id":"ff8bb61.4f49a48","type":"obniz-repeat","z":"523da830.10ea08","obniz":"a8808e2f.d98e9","name":"","interval":"1000","code":"var state = await obniz.switch.getWait();\r\nmsg.payload = state;\r\n\r\nreturn msg","x":240,"y":480,"wires":[["488a94d0.8dda7c"]]},{"id":"ca3a09ac.aed6f8","type":"debug","z":"523da830.10ea08","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":830,"y":480,"wires":[]},{"id":"488a94d0.8dda7c","type":"switch","z":"523da830.10ea08","name":"","property":"payload","propertyType":"msg","rules":[{"t":"neq","v":"none","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":510,"y":480,"wires":[["ca3a09ac.aed6f8"]]},{"id":"a8808e2f.d98e9","type":"obniz","z":"","obnizId":"","deviceType":"obnizboard","name":"main board","accessToken":"","code":""}]

まとめ

今回はNode-REDでobnizを動かしてみました。

obnizとのやり取りがよりシンプルになってとてもいいと思いました。

最初コードの書き方がよく分からず戸惑いましたが、functionノードを使っていたらその感覚でobniz.jsのコードを書くだけになるので、慣れれば何でもできそうな気がします。

これでNode-REDでobnizを動かしていたときのつらみが解消される気がします。

オレオレkivyチュートリアル~その2 ボタンを実装~

前回はkivyをパソコンに導入して、アプリを作成するところを紹介しました。

supernove.hatenadiary.jp

今回はボタンを押したら文字を変更するアプリを実装してみます。kivyのインストールがまだの場合は前回紹介しているセットアップを終えてから続きを進めてもらえるといいと思います。

ボタンの実装

それでは、前回のコードから修正を加えてボタンをとりあえず実装してみます。

コードは以下になります。

前回との違いはfrom kivy.uix.label import Labelfrom kivy.uix.button import Buttonに変更されて、sampleApp.buildの戻り値がButtonに変更されています。

from kivy.app import App
from kivy.uix.button import Button

class sampleApp(App):
    def build(self):
        return Button(text='Hello World!')

if __name__ == '__main__':
    sampleApp().run()

実行すると、以下のようにウィンドウ全体にボタンが表示されます。ボタンをクリックすると画面がフラッシュしたような挙動をします。

f:id:KMiura:20201005012813p:plain

何かアクションを加えてみる

それでは次にボタンを押したときのアクションを足してみます。

ボタンを押したら文字列がコンソールに出力されるようにします。

というわけでsampleAppクラスにメソッドを追加して以下のコードを動かしてみます。

from kivy.app import App
from kivy.uix.button import Button

class sampleApp(App):
    def build(self):
        return Button(on_press=self.pressed, text='Hello World!')
    def pressed(self, btn):
        print('pressed')

if __name__ == '__main__':
    sampleApp().run()

実行するとボタンを押したときにコンソール画面にpressedメソッドで指定した文字列が出力されます。 f:id:KMiura:20201005015638p:plain

BoxLayoutを使ってみる

ここまでは画面いっぱいに一つのボタンを用意しましたが、これでは他のUIを組み込むことができません。

そこで、BoxLayoutを使って画面に複数のUIを実装してみたいと思います。

BoxLayoutを使えば水平・垂直方向にUIを配置することができるようになります。

BoxLayout()の引数のorientationで配置する方向を設定することができます。(vertical:垂直方向、horizontal:水平方向)

また、add_widget()メソッドを使って、UIを配置することになります。

前回使ったラベルと組み合わせたコードが以下のとおりです。

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout

class sampleApp(App):
    def build(self):
        layout = BoxLayout(orientation='vertical')
        self.lb = Label(text='Hello World!')
        self.bt = Button(on_press=self.pressed, text='Push')
        layout.add_widget(self.lb)
        layout.add_widget(self.bt)
        return layout
    def pressed(self, btn):
        self.lb.text = 'pressed'

if __name__ == '__main__':
    sampleApp().run()

sampleApp.pressedのアクションをprint関数からラベルの表示を変更するテキストに変更しました。

実行すると、以下のようにボタンを押すとラベルのテキストが変更されます。

f:id:KMiura:20201008000418p:plain

ボタンを押しているときだけテキストを変更する

先程までの実装だとボタンを押したらテキストは変更されたままになるので、今度はボタンを押している間だけテキストが変更されるようにしてみます。

やり方としてはButton()オブジェクトのオプションでon_pressを指定したときと同様に、on_releaseにメソッドを指定するとボタンを離したときのメソッドを指定します。

self.bt = Button(on_press=self.pressed, on_release=self.released, text='Push')

また、sampleAppクラスに新たに以下のメソッドを追加します。

def released(self, btn):
    self.lb.text = 'Hello World!'

実行すると押している間だけ、ラベルのテキストが変更されてボタンを離すと元のテキストに戻るようになります。

f:id:KMiura:20201008001739p:plain

まとめ

今回はボタンを装備して、そのボタンに対して何かアクションを付ける方法を紹介しました。

ここまでのことをやれば例えばRaspberry Piを使ってGPIOの操作を実装することもできます。

次回はもっと自由自在にUIのレイアウトができるkv languageについて紹介します。

「freee / IBM Cloud / LINE API アプリ開発勉強会」に登壇してみた&サンプルアプリの補足

freeeさんの主催のアプリ開発勉強会に登壇しました。

freee.connpass.com

このイベントは只今絶賛開催中の「freeeアプリコンテスト」関連のイベントになっており、ここではfreeeのAPIを使ったアプリ開発についてサンプルアプリを交えて紹介しました。

speakerdeck.com

今回は僕のパートで紹介したアプリについての簡単な解説とセッション中に話せなかった補足的なお話をします。 個人的にはfreeeのAPIの仕様に苦しめられて作るのにめっちゃ時間がかかりましたが、反応はそこそこでやってよかったと思います。 (欲を言えばライブデモをやったので、その反応がもっと欲しかったw)

今回紹介したアプリは僕のGithubのレポジトリにソースコードが出ているので、中身についてはそちらを御覧ください。

github.com

今回作ったアプリ

freeeのアプリアワードのページのなかで紹介されている「みんなの困り事」の中から

  • 経費精算が大変
  • 打刻忘れが多い

といったことを手元のLINE botを解決してみましょうというものです。

www.freee.co.jp

実際の動作はこちらからご覧いただけます。

youtu.be

開発環境

  • Python
  • Flask
  • ngrok or heroku

以前の記事で紹介した動作環境とほぼ一緒で、ローカルでngrokを使うこともできますし、herokuにデプロイできるようにProcfileも用意しています。

supernove.hatenadiary.jp

今回のソースコードを見てもらうと.envで設定する環境変数がやたら多いですw。 というわけでこの補足記事では各環境変数をどこから持ってくればいいかを紹介します。

LINE Developersから取得する変数

Messaging API

CHANNEL_ACCESS_TOKENCHANNEL_SECRETは、Messaging APIのチャネルから取得します。 また、リッチメニューのIDATTEND_MENU_IDON_WORK_MENU_IDはREADMEのリッチメニューの登録APIを使うと出力されるリッチメニューIDをそれぞれ設定します。 ATTEND_MENU_IDは出勤時に表示させるリッチメニューIDで、ON_WORK_MENU_IDは業務時間中に表示するリッチメニューIDです。

LIFF

REGIST_LIFF_IDFIX_TIME_LIFF_IDはLINEログインチャネルを立ててLIFFを作成すると表示されているLIFF IDをコピーします。 管理がしやすいので、同じチャネルにLIFFを2つ作成してOKです。また設定するエンドポイントは以下の通りです。

  • REGIST_LIFF_ID:【アプリのURL】/regist
  • FIX_TIME_LIFF_ID:【アプリのURL】/fix_time

このときチャネルはユーザIDを共有するため、Messaging APIを立てたときと同じプロバイダーで立てます

Clova OCR

OCR_API_URLOCR_API_KEYについてはLINE Developersでは提供されていないサービスで別途用意する必要があります。 今回のアワードの参加者限定で無料で公開されるので、詳しくは以下の記事からご覧いただければと思います。

prtimes.jp

APIトークンとURLについては申請すると認証情報がもらえると思うので、もらったときにそれぞれ変数を設定しましょう。

Cloudant

認証情報

CloudantはIBM CloudでCloudantのリソースを作成しておきます。ちなみにNode-REDを使っている方はすでにCloudantのリソースがあるのでそちらを使ってもらってOKです。 リソースを開いたらサービス資格情報タブを選択して、新規資格情報を作成をクリックします。 役割の項目は「管理者」を選択した状態で追加をクリックして作成します。

f:id:KMiura:20201001010300p:plain

ここで生成されたURL、ユーザー名、パスワードをCLOUDANT_URLCLOUDANT_USERNAMEそしてCLOUDANT_PASSWORDに設定します。

データベースの設定

TOKEN_DOC_IDについては管理タブをクリックして、Launch Dashboardを開くとCloudantのダッシュボードが開きます。 そこから「Create Database」をクリックして、freee_tokensを作成します。同様にログ取り用にlabor_botも作成しておきます。

f:id:KMiura:20201001011201p:plain

その後、freee_tokensを開き、Create Documentをクリックしてドキュメントの作成画面が開きます。

このときIDが表示されるのでこのIDを.envのTOKEN_DOC_IDに設定します。

ドキュメントにはこちらで紹介されている、アクセストークンを取得するAPIを実行したときのレスポンスの中身を以下のように追加します。

追加したら、Create Documentをクリックしてデータベースの設定を完了させます。

f:id:KMiura:20201001012338p:plain

freeeの事業所ID

COMPANY_IDは先程取得したアクセストークンを使って、事業所IDの一覧を取得するAPIを実行したときのレスポンスに出てくるcompany_idを設定します。

IBM Cloud Functions

ここまで.envファイルに必要な変数を紹介しましたが、更にIBM Cloud Functionsにも追加で変数を設定をすることでfreeeのトークンの自動更新機能を動かすことができます。

こちらを開いて、アクションを作成します。

アクションの作成方法は以下の記事を御覧ください。パラメータ(環境変数)の設定方法も記載しています。

qiita.com

また、使用するコードはレポジトリ内のこちらのコードです。

パラメータは以下の値を設定します。

Parameter Name Parameter Value
CLIENT_ID freeeのアプリ管理画面から取得
CLIENT_SECRET freeeのアプリ管理画面から取得
URL CloudantのURL
PASSWORD Cloudantのパスワード
USERNAME Cloudantのユーザーネーム
TOKEN_DOC_ID .envに設定したもの

環境変数の設定

ngrokで動かす時は特に気にする必要はありませんがherokuでデプロイするには.envファイルの環境変数をheroku側に反映させる必要があります。 一つずつやるのはさすがに面倒なので以下のコマンドを.envファイルのあるディレクトリ上で実行すると環境変数をまとめて反映してくれます。

heroku config:push

まとめ

今回はイベントのデモで使ったコードの環境変数の設定について紹介しました。

ただ値を取るだけの話ですが、APIを使う上ではいろんなサービスを行ったり来たりしながらの作業なので自ずといろんなサービスを触って知ることになるので理解も深まると思います。

そして、また次に何かアプリを作りたいときに簡単にアイデアを形にしやすいです。

逆にAPIを作る側になると設計からリリースまでのプロセスがありとても大変なものです。(この話はよしましょうw。)

オレオレkivyチュートリアル~その1 インストールと簡単なアプリづくり~

今月末にPyCon JPがあるということでいい機会なので、3年前のPyCon JPで知ったGUIアプリのライブラリであるkivyのチュートリアルシリーズを始めたいと思います。

ちなみにその時のPyConJPの参加記事はこちら↓です。かなり初期に書いたのでひどい文章ですw。

supernove.hatenadiary.jp

このときのセッションでkivyの存在を初めて知って以来、大学時代の研究に使うアプリをいくつか作っていたりしました。黒を基調にしたモダンなデザインのアプリができるので、個人的にはかなり好んで使っていました。

今はエンジニアとしてサーバーサイドの開発がメインになりクライアントサイドの開発がご無沙汰なので、改めて使い慣れたPythonでデスクトップのクライアントアプリを作ってみたいと思います。

Kivyとは

まずそもそもKivyってなに?ってところからだと思いますので、そこから簡単に話します。KivyはPythonGUIアプリを作る際に使用されるライブラリです。マルチぷらっとフォームとなっており、WindowsMacLinuxでの開発にサポートしています。

PythonGUIアプリといえばTkinterが有名ですが、Tkinterとは大きく違うのがスマホアプリを作ることができます。さらにTkinterよりも自由なレイアウトを作成できるのと拡張UIパーツが豊富なので、思い通りのアプリが実装できます。

インストール

インストール方法はOpenGLSDLを使用するので、OSによって依存パッケージのセットアップ方法が様々なので以下の公式ドキュメントからお使いのOSに合わせてインストールします。

kivy.org

ここではRaspberry Piで動かしてみたいと思うので、Raspberry Piでのインストール方法を紹介します。今回作成するアプリはどのOSでも動くはずなので、他のOSでインストールしてもOKです。

公式ドキュメントでは、Raspbian Stretchまでのインストール方法しか紹介されていませんでしたが、一応Raspbian BusterでもStretchと同じ方法でインストールできました。Busterにデフォルトで導入されているのがPython3.7なので問題ないかと思います。

まずは、以下のコマンドを実行し依存パッケージをインストールします。

sudo apt update
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
   pkg-config libgl1-mesa-dev libgles2-mesa-dev \
   python-setuptools libgstreamer1.0-dev git-core \
   gstreamer1.0-plugins-{bad,base,good,ugly} \
   gstreamer1.0-{omx,alsa} python-dev libmtdev-dev \
   xclip xsel libjpeg-dev

続いて、以下のコマンドを実行してPythonの依存ライブラリをインストールします。

pip3 install --upgrade --user pip setuptools
pip3 install --upgrade --user Cython==0.29.10 pillow

最後に以下のコマンドを実行して、kivyをインストールします。

pip3 install --user kivy

アプリ作成

まずは画面出力

それではkivyを使ってアプリ開発を始めてみます。アプリを動かすためには画面を出力させる必要があります。以下のコードを適当なディレクトリに保存します。

# coding: utg-8
from kivy.app import App

App().run()

実行するとこんな感じで何もない真っ黒な画面が表示されます。

f:id:KMiura:20200816113731p:plain

最小構成であればたった2行でアプリを作れてしまいます。

何か文字を出力する

画面を表示させることができましたが、アプリを作る上ではこの画面に対してUIを足していく必要があります。そこで次に画面上に文字を表示させることをやってみます。ここからがお約束のHello Worldです。

画面を表示するだけなら先程の2行だけで書けると言いましたが、UIを作成するときにはAppクラスを継承してクラス文で記述する必要があります。

# coding: utf-8
from kivy.app import App
from kivy.uix.label import Label

class sampleApp(App):
    def build(self):
        return Label(text='Hello, World!')

if __name__ == "__main__":
    sampleApp().run()

実行すると以下のように画面の真ん中にHello World!と表示されます。

f:id:KMiura:20200816120712p:plain

ここで先程のなにもない画面と比較すると、ウィンドウのタイトルが「名称未設定」だったのが、このコードを実行すると「sample」と表示されました。

これはクラス名の中のAppより前の文字列をそのままアプリウィンドウのタイトルとして自動で表示させています。

もしもタイトルを別で表示させるときには、sampleAppクラスに__init__()メソッドを追加してself.title = 'title'という感じでタイトルを追加できます。

class sampleApp(App):
    def __init__(self, **kwargs):
        super(sampleApp, self).__init__(**kwargs)
        self.title = 'title'
    def build(self):
        return Label(text='Hello, World!')

今回はここまで!

今回はKivyの最初の導入部分を行い、Hello Worldをやりました。Pythonはサーバーサイドで使うことが多いですが、昔からGUIアプリのライブラリが豊富に存在しています。

Kivyはその中でも自由自在にレイアウトができるので、自分好みのかっこいいアプリを作りやすいです。

次回はKivyを使ってボタンを配置する方法や、ボタンを押したときのアクションの作成方法を紹介します。

ヤフー名古屋 Tech Meetup LT #1 - LT会「リモートワーク環境」に登壇してみた&もう少しキーボードについて語りたい

前回に続いて今回もLT登壇の話です。今回はヤフー名古屋主催のオンラインLTイベントに参加しました。テーマは「リモートワーク環境」。僕は業務が長引いていたこともあり、途中からの参加になりました。

yahoo-nagoya.connpass.com

今回は「社会人1年目のリモートワーク」というテーマで最近整えたリモートワーク環境についてのお話をしました。というよりもほぼキーボードについての話がメインになりましたw。その時の資料はこちら↓

speakerdeck.com

資料では結構前フリが長めでしたがキーボードの話をしたさでこの部分はかなり巻き気味で話しました。本編では、今も仕事でメインキーボードとして使っている東プレのRealforceについて話しました。今使っているRealforceはシリーズの中で一番安いテンキーレスモデルで家で使うには十分なキーボードです。1ヶ月使っていてとても快適でした。

発表終わったあとの質疑応答でキーボードのあれこれを話していましたが、実はこの資料ができたあとにAmazonのタイムセールで安くなっていたのでよくRealforceと比較されるHHKBのBluetoothモデルも買ってしまいましたw。

HHKBを買ったのはただ興味があったからではなく、持ち運び用のキーボードとして買うことにしました。出先でパソコンを使ったりオフィスに出社するときもキーボードは妥協したくないと思ったわけです。というわけでここからはこのHHKBとRealforceを使ってみての比較をしてみます。

外観

Realforceは最新のモデルで小さくなったとはいえ、テンキーレスでも場所はとります。ただ、テンキー以外のフルサイズキーボードに使われているキーもあったりしてそこは便利だと思います。一方でHHKBは必要最小限のキーだけを配置しているのでかなり小さいです。2つ並べて大きさを比較すると全然違います!

f:id:KMiura:20200729234032j:plain

写真からお気づきかと思いますが、HHKBについては文字が印字されていない無刻印のキーです。最初は買うときに躊躇しましたが、最近ブラインドタッチをやっとできるようになったし多分いけるかなと思ったのと、ブラックのキーボードが欲しかったのでこちらのモデルを選びました。

重さ

HHKBの重さは531gあります。大体500mlのペットボトルの飲み物と同じ重さと考えると持ち運びには問題ない重さですね。一方でRealforceなんと1.4kgもあります。macbook proの13インチが1.3kgぐらいなので持ち運ぶとなると実質macbook2台分持ち運ぶということになるんですね。さすがに気が引ける…。一度だけオフィスに持ち込んで使ってみましたが、デスクでずっと使い続ける分にはいいです。ただ、ミーティングなどで席を移動するときにキーボードも一緒に持つとめちゃくちゃ重くて大変でした。macbook2台分を持っていたこと考えるとそりゃ重いわけです。

接続

RealforceシリーズのキーボードはすべてUSB接続のみです。もともと持ち運び用のキーボードとしてではなくて業務用として販売しているものなので仕方がないと思います。むしろ家で使うつもりで買ったものなので有線のキーボードが条件にぴったりでした。HHKBはUSB接続とBluetoothの両方があります。最近のモデルだと1台でUSB接続とBluetooth接続のどちらも使えるHybridモデルと言われるものも販売されています。今回買ったものは旧モデルでBluetooth接続のみとなっています。逆にこちらは外で使いたくてあまりケーブルがかさばらないものが欲しかったので、これで問題ないです。そして、キーボードの電源はどこでも手に入る単3電池2本でも動きますし、microUSBケーブルを使ってパソコンから給電する事もできます。これはHHKBを長く使ってもらうための設計の一つで仮にリチウムイオン電池を内蔵してしまうとキーボードの寿命が電池で決まってしまうため、乾電池を交換する仕組みを取り入れているというわけです。そして、仮に電池が切れてもUSBで給電できるようにしているのもポイントが高いです。

打ち心地

キーボードなので、やはり肝心なのは実際の使ったときの感触ですよね。というわけで、それぞれの打ち心地を比較してみます。ただ、キースイッチの重さが違うので厳密な比較にはなりませんが参考程度に見てもらえばと思います。まずRealforceですが、僕の使っている機種はキースイッチの重さが30gでめちゃくちゃ軽いので、あまり指に力を入れずに押し込めます。逆を言えばちょっと指を乗せただけでご入力をしてしまうので、キーボードに手をおいてぼーっとしていたら大量に入力されるということがしばしばありましたw。軽く、深く押せるので入力中の指の移動がスムーズでいい意味で癖のないキーボードです。

そして、HHKBのキーの荷重は全てのモデルが45gになっています。なので、Realforceで慣れていると打っていてかなり重く感じました。さらにHHKB特有のFnキーと組み合わせた操作はやはり最初は戸惑いましたが、まだ使い始めて間もないのでこれから慣らしていくものになると思います。さらに先程述べたように今使っているのが無刻印のキーボードなので、文字はともかく数字や記号を入力するときは「えっ、どれだっけ…」となることがあります。それでもまだ許容範囲で業務に支障をきたすほどでは無いので、これからどんどん使い込んでいけば馴染むだろうなという期待は持てます。

実際の打鍵がどんな感じなのか参考までにキーボードの打鍵音の比較動画を載せたのでぜひ比較してみてください。前半がRealforceで後半がHHKBです。

youtu.be

ご存知かと思いますが、RealforceもHHKBも使用しているキースイッチが静電容量無接点を採用しており、打鍵音に大きな違いはないかと思います。個人的にはこれぐらいの打鍵音が気持ちよく耳に入る音だと思います。さらにノートパソコンのキーボードと比べるとキーを深く押せるので、指への負担がだいぶなくなり前に感じていた手の疲れも減りました。なにより、どちらのキーボードを使っていてもたくさん打ち込んでみたくなるので値段相応の価値があると思います。

※これはあくまで個人的な見解なので、自分にあったキーボードかどうかはお店で絶対に試し打ちをしましょう!

まとめ

  • 家で使うならRealforce

  • 外に持ち運ぶならHHKB

家ではとにかく有線で確実につながるものを使いたいのでRealforceがいいと思います。そして、HHKBはコンパクトで使う場所を選ばないというところがいいところだと思うので出かけるときのお供に持っておきたいキーボードです。どちらもそれぞれにしか出せない使い心地があるので、これからガシガシ使い込んでいきたいと思います。