ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

もしデータベースのトランザクションが使えなかったら

こんにちは、システム開発部のちょうです。

毎日バックエンドの開発に一番馴染みのあるものがデータベース、そしていろんな機能はトランザクションベースで開発されたのです。トランザクションはデータベース基本の機能で、トランザクションなしにはデータベースだと言えないぐらい重要です。でももしトランザクションが使えなくなったら、みんな開発できなくなるでしょうか。答えはNoです。実際、

  • トランザクションだけで対応できない
  • データベースにトランザクション機能がない
  • 分散システムでトランザクションが使えない

ようなケースは珍しくありません。

続きを読む

Perceptual Hashを使って画像の類似度を計算してみる

最近、引越しをしたWebエンジニアの本間です。 引越しの作業は大変面倒でしたが、新しい街に来た時のワクワク感がやっぱりいいなーと感じております。

さて、弊社のサービスである「写真サービス るくみー」では、毎日たくさんの写真をアップロードしていただいているのですが、中には内容がほとんど同じ写真が入ってしまうことがあります。 これらの写真がそのまま販売されてしまうと、写真を選ぶ際に邪魔になったり、間違って複数枚購入してしまうことがあるため、可能な限り避けたい事象です。 「同じ内容」の写真を自動で判別する方法がないか調査していたところ「Perceptual Hash」という手法を見つけました。 Pythonでの画像処理の勉強も兼ねて、今回この手法を紹介してみようと思います。

続きを読む

SupersetをECS(Docker)で導入してみる

おはこんばんちはユニファのインフラのすずきです。

今肩こりが悪化しすぎて右腕が痛くなって接骨院通っています。 右腕の痛みは治ったのですが肩のほうが痛くないのが異常だと言われ緩めるために引き続き通い続けています。 みなさんも普段の姿勢とか肩を動かす運動とかして慢性的な肩こりにならないようにしていただけたらと思います。

あとiPhoneX買いましたanimojiキモいですね!

さて本題、弊社DBの情報をグラフなどで可視化したり、一覧で取得したりなどするためにRedashを利用しています。 特定のサービスでしか利用していなかったので、先日他のサービスでも利用できるように共通の環境に移設しました(ついでにDocker化)。
ただ、Redash自体は他の候補を検討せず導入したものであり、実現したいことも増えてきたのでこれを期にRedash以外を検討してみようということになりました。
そこで、最近良く目にするようになってきたSupersetがどんなものかお試しで利用してみようとなりました。 丁度RedashがDockerで動いてるのでSupersetもDockerで動かしてみようとおもったので、その内容をブログにしてみました。

続きを読む

SORACOM Inventory によるデバイス管理(Limited Preview お試し)

 こんにちは、ユニファの赤沼です。

 今年の7月に SORACOM Inventory が発表され、 Limited Preview の受付が開始されました。ユニファでもすぐに申し込んでいたのですが、先日利用案内をいただくことができたので早速試してみました。案内をいただくまでに4か月ほどかかっていますが、それだけ申し込みが多く注目度が高いということなのかと思います。

soracom.jp

SORACOM Inventory とは

f:id:akanuma-hiroaki:20171110091212p:plain:right:w150

 詳細については公式サイトをご覧いただければと思いますが、一言で言うとデバイス管理サービスです。 SORACOM Air により回線に接続したデバイス上で LwM2M に対応したエージェントを動作させることで、デバイスの自動登録やデバイスの管理が可能になります。

 Limited Preview 中は無料で利用することができますが、本サービス利用時には下記サイトにあるような料金が発生します。

soracom.jp

SIMグループの設定

 SORACOM Inventory を Limited Preview で使うには、利用する SIM が所属するSIMグループを、あらかじめ用意された Limited Preview 専用の VPG(Virtual Private Gateway) に API を使って紐づける必要があります。SORACOM の API は Reference ページから実際に API にリクエストを投げられるようになっていますので、下記のようにSIMグループの設定変更の API にリクエストを投げます。下記画像ではグループIDと VPGID はマスクしていますが、実際には対象のグループIDと SORACOM から案内のあった VPGID を指定します。

f:id:akanuma-hiroaki:20171109211634p:plain

エージェントのインストール

 次に各デバイスで動作させるエージェントをインストールします。 Limited Preview ではエージェント実装のサンプルとして、 Eclipse Wkaama ベースの C エージェント、 Java エージェント、 Android エージェントが配布されます。今回はまず Raspberry Pi 上で C エージェントを動かしてみます。

 まずはエージェントのビルドに必要な cmake をインストールします。

$ sudo apt-get install cmake

 次に Eclipse Wakaama を clone し、 submodule をセットアップします。

pi@raspberrypi:~/tmp $ git clone https://github.com/eclipse/wakaama
Cloning into 'wakaama'...
remote: Counting objects: 5924, done.
remote: Total 5924 (delta 0), reused 0 (delta 0), pack-reused 5924
Receiving objects: 100% (5924/5924), 2.13 MiB | 444.00 KiB/s, done.
Resolving deltas: 100% (4194/4194), done.
Checking connectivity... done.
pi@raspberrypi:~/tmp $ 
pi@raspberrypi:~/tmp $ cd wakaama/
pi@raspberrypi:~/tmp/wakaama $ 
pi@raspberrypi:~/tmp/wakaama $ git submodule init
Submodule 'platforms/Linux/tinydtls' (https://git.eclipse.org/r/tinydtls/org.eclipse.tinydtls) registered for path 'examples/shared/tinydtls'
pi@raspberrypi:~/tmp/wakaama $ 
pi@raspberrypi:~/tmp/wakaama $ git submodule update
Cloning into 'examples/shared/tinydtls'...
remote: Total 343 (delta 0), reused 343 (delta 0)
Receiving objects: 100% (343/343), 443.92 KiB | 211.00 KiB/s, done.
Resolving deltas: 100% (110/110), done.
Checking connectivity... done.
Submodule path 'examples/shared/tinydtls': checked out '0016138fe3998552eee3987a1c09da43a23c9fb5'

 続いて、 wakaama に SORACOM Inventory 用のパッチを適用します。 Limited Preview の案内と同時に配布されたパッチの圧縮ファイルを適当なディレクトリに展開してビルドします。(出力は省略)

pi@raspberrypi:~/tmp/wakaama $ tar zxf wakaama_patch.tgz
pi@raspberrypi:~/tmp/wakaama $ cd ../
pi@raspberrypi:~/tmp $ mkdir build
pi@raspberrypi:~/tmp $ cd build/
pi@raspberrypi:~/tmp/build $ cmake -DDTLS=1 ../wakaama/examples/client
pi@raspberrypi:~/tmp/build $ make

デバイスの登録

 それではエージェントを起動してデバイス情報を登録してみたいと思います。 SORACOM Air でネットワークに接続した上で、下記のようにエージェントを起動します。 State が STATE_READY になれば成功です。

pi@raspberrypi:~/tmp/build $ ./lwm2mclient -b -n raspberry_pi -h bootstrap.soracom.io
Using MAC address b827ebb3dcad as endpoint name
Trying to bind LWM2M Client to port 56830
LWM2M Client "raspberry_pi" started on port 56830
>  -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_BOOTSTRAPPING
 -> State: STATE_REGISTERING
 -> State: STATE_REGISTERING
 -> State: STATE_REGISTERING
decrypt_verify(): found 24 bytes cleartext
 -> State: STATE_REGISTERING
decrypt_verify(): found 22 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 19 bytes cleartext
 -> State: STATE_READY

データ確認

 エージェントから登録されたデバイス情報は SORACOM コンソールから確認できます。

f:id:akanuma-hiroaki:20171110064638p:plain

f:id:akanuma-hiroaki:20171110063814p:plain

 上記は一部ですが、他にも LwM2M の仕様に沿って多くの項目が表示されていて、バッテリーレベル、通信強度、緯度、経度などの項目が確認できます。ただし、それぞれの情報が取得できるか、どんな内容が取得できているかはエージェントの実装に依存します。 Limited Preview の案内で配布されているエージェントは検証用のサンプルなので、実際に本番サービスに使用する場合は自前で使用するデバイスに対応したエージェントを実装する必要があります。

Java エージェントによる登録・確認

 Java版のエージェントでも動作を確認してみます。手元の Raspberry Pi には Java がインストールされていなかったので、まずは Java をインストールします。

pi@raspberrypi:~ $ sudo apt-get install oracle-java8-jdk

 Limited Preview ではビルド済みの Java エージェントが配布されていますので、解凍すれば実行できます。

pi@raspberrypi:~/tmp $ unzip soracom-inventory-agent-example-0.0.1.zip 

 SORACOM Air でネットワークに接続した上で、下記のようにエージェントを実行します。

pi@raspberrypi:~/tmp $ cd soracom-inventory-agent-example-0.0.1/bin/
pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ./soracom-inventory-agent-example-start -e raspberry_pi
2017-11-06 23:21:36,348 INFO SORACOMInventoryAgentExample - using endpoint [raspberry_pi]
2017-11-06 23:21:37,757 INFO InventoryAgentInitializer - set bootstrap security object.
2017-11-06 23:21:37,771 INFO InventoryAgentInitializer - set instance to initializer.objectId:3 instance num:1
2017-11-06 23:21:37,772 INFO InventoryAgentInitializer - set instance to initializer.objectId:14 instance num:1
2017-11-06 23:21:39,148 INFO LeshanClient - Starting Leshan client ...
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.CoapServer start
INFO: Starting server
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coaps://0.0.0.0:0
Nov 06, 2017 11:21:39 PM org.eclipse.californium.scandium.DTLSConnector start
INFO: DTLS connector listening on [0.0.0.0/0.0.0.0:48720] with MTU [1,280] using (inbound) datagram buffer size [16,474 bytes]
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coaps://0.0.0.0:48720
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coap://0.0.0.0:0
Nov 06, 2017 11:21:39 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coap://0.0.0.0:50733
2017-11-06 23:21:39,319 INFO LeshanClient - Leshan client started [endpoint:raspberry_pi].
2017-11-06 23:21:39,353 INFO RegistrationEngine - Trying to start bootstrap session to coap://100.127.127.100:5683 ...
2017-11-06 23:21:39,834 INFO RegistrationEngine - Bootstrap started
2017-11-06 23:21:40,242 INFO RegistrationEngine - Bootstrap finished ServersInfo [bootstrap=Bootstrap Server [uri=coaps://beam.soracom.io:5684], deviceMangements={123=DM Server [uri=coaps://jp.inventory.soracom.io:5684, lifetime=60, binding=U]}].
2017-11-06 23:21:40,244 INFO BootstrapObserver - Bootstrap success: coap://100.127.127.100:5683
2017-11-06 23:21:40,247 INFO BootstrapObserver - Bootstrap server: Bootstrap Server [uri=coaps://beam.soracom.io:5684], Device management servers: {123=DM Server [uri=coaps://jp.inventory.soracom.io:5684, lifetime=60, binding=U]}
2017-11-06 23:21:40,287 INFO FileCredentialStore - save credential to .soracom-inventory-credentials.dat
2017-11-06 23:21:40,291 INFO RegistrationEngine - Trying to register to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:21:42,966 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:21:42,969 INFO RegistrationEngine - Registered with location '/rd/lWFi3sMHIp'.
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.network.config.NetworkConfig store
INFO: writing properties to file /home/pi/tmp/soracom-inventory-agent-example-0.0.1/bin/Californium.properties
2017-11-06 23:21:43,378 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
2017-11-06 23:21:43,378 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#7b184646f5c054fb and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#48f72e0076f29416 and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#086126447a85b86a and resource /1
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#97c1ac3d635941e2 and resource /3
Nov 06, 2017 11:21:43 PM org.eclipse.californium.core.server.ServerMessageDeliverer deliverRequest
INFO: Did not find resource [6, 0, 5] requested by jp.inventory.soracom.io/52.198.95.62:5,684
2017-11-06 23:21:53,376 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:21:53,377 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:22:36,970 INFO RegistrationEngine - Trying to update registration to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:22:37,896 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:22:37,897 INFO RegistrationEngine - Registration update succeed.
2017-11-06 23:22:53,376 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:22:53,377 INFO ObservableInventoryObjectEnabler - fire resource change for observation.

 登録された情報をコンソールから確認してみます。

f:id:akanuma-hiroaki:20171110065601p:plain

 Endpoint はエージェントの起動コマンドで同一の値を渡しているので同じになっていますが、そのほかの項目では C のエージェントで登録した場合と比べて、取得できている項目や内容に違いがあります。これはそれぞれのエージェントの実装で取得している内容が異なっているためと思われます。

Wi-Fi 接続でのデータ取得

 SORACOM Inventory では大きく分けて、下記の2つのステップがあります。

  • デバイスの登録

  • デバイスの管理(データ取得、コマンド実行)

 デバイスの登録については SORACOM Air で回線に接続して行う必要がありますが、登録時にそれ以降の通信用の鍵の交換などが行われますので、一度登録が完了すればデバイスの管理は Wi-Fi 等、 SORACOM Air 以外の接続方法でも行うことが可能です。例えば Limited Preview で配布されている Java エージェントでは、デバイス登録時に下記のような認証用ファイルが作成されます。

pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ls -l .soracom-inventory-credentials.dat 
-rw-r--r-- 1 pi pi 460 Nov  6 23:21 .soracom-inventory-credentials.dat

 上記ファイルがある状態でエージェントを実行すると、デバイスの登録のステップは実行されず、データの更新のステップから実行され、 Wi-Fi 接続でも下記のように実行可能です。

pi@raspberrypi:~/tmp/soracom-inventory-agent-example-0.0.1/bin $ ./soracom-inventory-agent-example-start -e raspberry_pi
2017-11-06 23:27:25,390 INFO SORACOMInventoryAgentExample - using endpoint [raspberry_pi]
2017-11-06 23:27:26,161 INFO FileCredentialStore - load credential from .soracom-inventory-credentials.dat
2017-11-06 23:27:26,162 INFO InventoryAgentInitializer - set security object from credential.
2017-11-06 23:27:26,171 INFO FileCredentialStore - load credential from .soracom-inventory-credentials.dat
2017-11-06 23:27:26,174 INFO InventoryAgentInitializer - set instance to initializer.objectId:3 instance num:1
2017-11-06 23:27:26,174 INFO InventoryAgentInitializer - set instance to initializer.objectId:14 instance num:1
2017-11-06 23:27:26,912 INFO LeshanClient - Starting Leshan client ...
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.CoapServer start
INFO: Starting server
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coaps://0.0.0.0:0
Nov 06, 2017 11:27:26 PM org.eclipse.californium.scandium.DTLSConnector start
INFO: DTLS connector listening on [0.0.0.0/0.0.0.0:48720] with MTU [1,280] using (inbound) datagram buffer size [16,474 bytes]
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coaps://0.0.0.0:48720
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Starting endpoint at coap://0.0.0.0:0
Nov 06, 2017 11:27:26 PM org.eclipse.californium.core.network.CoapEndpoint start
INFO: Started endpoint at coap://0.0.0.0:52646
2017-11-06 23:27:27,006 INFO LeshanClient - Leshan client started [endpoint:raspberry_pi].
2017-11-06 23:27:27,043 INFO RegistrationEngine - Trying to register to coaps://jp.inventory.soracom.io:5684 ...
2017-11-06 23:27:29,016 INFO RegistrationEngine - Next registration update in 54.0s...
2017-11-06 23:27:29,021 INFO RegistrationEngine - Registered with location '/rd/H12GIitHqU'.
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.network.config.NetworkConfig load
INFO: loading properties from file /home/pi/tmp/soracom-inventory-agent-example-0.0.1/bin/Californium.properties
2017-11-06 23:27:29,225 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
2017-11-06 23:27:29,238 INFO ObservableInventoryObjectEnabler - observe start. observeStartDelayMillis:10000 observeInternalMillis:60000
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#11e177e05af7f852 and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#654af6b769a78c4b and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#28587f4d02931bb6 and resource /3
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.server.ServerMessageDeliverer deliverRequest
INFO: Did not find resource [6, 0, 5] requested by jp.inventory.soracom.io/52.198.95.62:5,684
Nov 06, 2017 11:27:29 PM org.eclipse.californium.core.CoapResource addObserveRelation
INFO: Successfully established observe relation between jp.inventory.soracom.io/52.198.95.62:5684#8801847f7f072340 and resource /1
2017-11-06 23:27:39,220 INFO ObservableInventoryObjectEnabler - fire resource change for observation.
2017-11-06 23:27:39,239 INFO ObservableInventoryObjectEnabler - fire resource change for observation.

 ちなみに Limited Preview で配布されている C のエージェントでは、 SORACOM Air 以外での接続での管理はサポートされていません。

コンソールからのコマンド実行

 SORACOM Inventory ではデバイスのデータを確認するだけでなく、デバイスに対してコマンドを実行することができます。これも実際にどんなコマンドが実行できるかはエージェントの実装に依存しますが、 Limited Preview の C エージェントではデバイスの再起動ができます。SORACOM Air で回線に接続して C エージェントを起動した上で、コンソールのデバイス詳細画面の reboot の項目の右側にあるボタンをクリックします。

f:id:akanuma-hiroaki:20171110081518p:plain

 下記のような確認ダイアログが表示されますので、再起動して問題なければ コマンド実行 をクリックします。

f:id:akanuma-hiroaki:20171110082020p:plain

 すると C エージェントの出力が下記のように続き、 Raspberry Pi が再起動されます。

decrypt_verify(): found 9 bytes cleartext
 -> State: STATE_READY
decrypt_verify(): found 18 bytes cleartext

         REBOOT

 -> State: STATE_READY
 -> State: STATE_READY
〜〜〜中略〜〜〜
-> State: STATE_READY
 -> State: STATE_READY
reboot time expired, rebooting ...Connection to raspberrypi.local closed by remote host.
Connection to raspberrypi.local closed.

まとめ

 IoT デバイスを用いたサービスを展開しようとした場合、デバイスの管理は大きな課題であり、デバイスの状態を詳しく把握できるかどうかが問い合わせ対応時等のユーザ満足度にも大きく影響します。自前でデバイス管理の仕組みを作ろうと思うと大変ですが、 SORACOM Inventory を使うことでデバイスの管理はかなりやりやすくなりそうに思えます。デバイスが IoT Gateway などを介してネットに接続している場合でも、 Gateway が SORACOM Air でネットに接続していればデバイスの登録は可能な点や、一度登録してしまえば Wi-Fi 等の接続でも運用できるのも嬉しいところです。エージェントを自前で実装する必要がある点や、デバイス上でエージェントを動作させる必要がある点はサービス設計時に考慮が必要ですが、それ以外の対応が大幅に減らせることを考えれば、 SORACOM Inventory を利用するメリットは大きいと思います。

Vision.framework を使って QR コードを読む

こんにちは、iOSエンジニアのしだです。
急に寒くなってきて秋をすっ飛ばしていきなり冬になってしまった感じがします。

iOS11 で追加された Vision.framework を使ってQRコードを読み込みしたいと思います。(n番煎じ感あります。)

QR コード

iOSで QRコード を読む場合2種類あります。

  • AVFoundation.framework (AVCaptureMetadataOutputObjectsDelegate)
  • Vision.framework

AVCaptureMetadataOutputObjectsDelegate は AVFoundation 内でキャプチャ中に検出するのに対して、Vision.framework ではキャプチャした画像に対して検出する違いがあります。
あと、AVCamBarcode サンプルコードを動かした感じ一度に4つのQRコードが検出されましたが、Vision.framework の方は時間はかかりますが 4つ以上は検出できました。

できたもの

用意したQRコードは、1から16の文字列をQRコードにしたものです。そして検出されたQRコードに緑枠を表示するシンプルなものです。
iPad mini 2 で試しましたが、16個検出するのに1分以上かかりました。

f:id:unifa_tech:20171031140022j:plain

Vision.framework 使い方

Requests を作って、RequestHandlerで実行して、Observations で結果を受け取るようになっています。

Requests

  • VNDetectFaceRectanglesRequest: 顔検出
  • VNDetectBarcodesRequest: QRコードなどのバーコード検出
  • VNDetectTextRectanglesRequest: テキスト検出
  • VNTrackObjectRequest: オブジェクト検出
  • など...

これらの Requests を作成して、 RequestHandler で実行します。そうすると Requests と対になる Observation が返ってきます。
例えば、 VNDetectFaceRectanglesRequest の場合 VNFaceObservation が結果として受け取れます。

バーコード検出

AVCaptureVideoDataOutput から受け取った CVPixelBuffer からQRコードを検出してます。検出したQRコードに緑枠を描画するために結果を AVCaptureVideoPreviewLayer のビューに渡しています。

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }

        let handler = VNSequenceRequestHandler()
        let barcodesDetectionRequest = VNDetectBarcodesRequest { [weak self] (request, error) in
            if let error = error {
                NSLog("%@, %@", #function, error.localizedDescription)
            }
            guard let results = request.results else { return }
            
            ...

            DispatchQueue.main.async {
                let barcodes: [VNBarcodeObservation] = results.flatMap { $0 as? VNBarcodeObservation }
                self?.previewView.barcodes = barcodes
            }
        }
        try? handler.perform([barcodesDetectionRequest], on: pixelBuffer)
    }
}

検出したQRコードに緑枠を付ける

VNBarcodeObservation には、検出した文字列(payloadStringValue)や CIBarcodeDescriptor を持っています。 また矩形の座標をもっているので描画します。

class PreviewView: UIView {
    let targetLayerName = "box"

    /// バーコードを設定したら画面更新
    var barcodes: [VNBarcodeObservation] = [] {
        didSet {
            self.setNeedsDisplay()
        }
    }

    ...

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        /// 緑枠クリア
        self.layer.sublayers?
            .filter { $0.name == targetLayerName }
            .forEach { $0.removeFromSuperlayer() }

        guard let previewLayer = self.layer as? AVCaptureVideoPreviewLayer else {
            return
        }

        for barcode in barcodes {
            /// VNRectangleObservation が持っている座標がY軸反転しているのでアフィン変換する
            let t = CGAffineTransform(translationX: 0, y: 1)
                .scaledBy(x: 1, y: -1)
            let tl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topLeft.applying(t))
            let tr = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topRight.applying(t))
            let bl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomLeft.applying(t))
            let br = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomRight.applying(t))
            
            /// 緑枠を描画
            let l = rectangle(UIColor.green, topLeft: tl, topRight: tr, bottomLeft: bl, bottomRight: br)
            self.layer.addSublayer(l)
        }
    }

    func rectangle(_ color: UIColor, topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) -> CALayer {
        let lineWidth: CGFloat = 2
        let path: CGMutablePath = CGMutablePath()
        path.move(to: topLeft)
        path.addLine(to: topRight)
        path.addLine(to: bottomRight)
        path.addLine(to: bottomLeft)
        path.addLine(to: topLeft)
        let layer = CAShapeLayer()
        layer.name = targetLayerName
        layer.fillColor = UIColor.clear.cgColor
        layer.strokeColor = color.cgColor
        layer.lineWidth = lineWidth
        layer.lineJoin = kCALineJoinMiter
        layer.path = path
        return layer
    }
}

おわりに

Vision.framework であれば複数のQRコードを検出できるのですが、検出数によって処理の時間が変わってきます。 4つ検出するだけであれば1秒くらいで検出できましたが、9つQRコード検出には10秒以上かかっていました。
同時にQRコードを検出することはないと思いますがなにかあれば参考にしたいと思います。

コードはこちらにあります。

bitbucket.org

参考

Zoomの面白機能あれこれ

こんにちは。エンジニアの田渕です。

急に寒くなった影響で、我が家の防寒装備が間に合っていません。。。>_<

周囲にも風邪をひく方が増えてきて、気づけば巷はハロウィン、もう今年も終わりに近づいているのだなぁと感慨深く。。。

さて、今回のエンジニアブログは、Web会議システムであるZoomの、あまり知られていない機能についてお話したいと思います。

Zoomって?

ZoomはWeb会議システムです。

zoom.us

ユニファはオフィスが名古屋と東京にある、また、エンジニアは自宅勤務が可能となっている環境であるため、 遠隔地に居る方と会議をする機会が多くあります。 オンライン英会話等ではSkypeを利用している例が多いですが、ユニファではZoomも利用しています。 Zoomに関する一般的な使い方の説明は、既に色々な方が記事を書いていらっしゃいますので、今回は割愛。

Zoom developers

最近ではWeb会議システムのソフトも色々とあり、無料で利用できるものから有料のものまで、選択肢が色々とあります。 選ぶ際には各ソフトの機能などを見て考えることが多いかと思いますが、Zoomは既に用意されているアプリを利用して会議を行うだけでなく、サービスを利用しているエンジニアが独自にZoomのアプリを開発するためのライブラリやSDKが用意されています。

Zoom Developers – Power Up Your Apps with Video, Voice, and Screen Sharing

今回はその仕組みを利用し、ちょっと変わったものを作ってみたいと思います。

Zoom Webclient

Zoom Webclientは、少々変わった用途で用意されています。

Webclient – Zoom Developers

f:id:unifa_tech:20171027120045p:plain
Zoom developersのトップページ。
ZoomはPCで利用する際に専用のソフトをインストールする必要がありますが、環境によっては

  • PCの管理者権限を持っていなかったりしてソフトがインストール出来ない場合

  • ネットワーク上のセキュリティの制限等で、PCから音声を送ることが出来ない場合

なんかもありますよね。 そんな場面を想定し、相手からの画面共有はPCで受け、音声通話は電話回線を通して行うことができるというのが、このWebclientです。 なんだそれ?とお思いの方も多いでしょう……。百聞は一見に如かずということで、さっそくSampleコードを動かしてみましょう。

Sampleを動かす!

Zoomの各SDK,ライブラリには、サンプルコードが提供されており、基本的にはdevelopersサイトに書かれている手順に従い動かせば動くようになっています。 WebclientのサンプルはHTMLとjs,cssから構成されていて、これらのコードをサーバー上で動かし、そのページを閲覧することで機能を利用することができます。 動かし方については公式ページのチュートリアルを見て頂くとして、今回は動かしたあとのページを見て頂こうと思います。

f:id:unifa_tech:20171027163421p:plain

↑は、Firefoxでページを見てみたところです。ローカルのサーバー上で動いています。 (画面上の文言や色などは、サンプルのオリジナルの画面から加工して変えています。) Zoomは開催するミーティングにそれぞれ固有の番号が振られますが、この画面で自分の名前と、そのミーティングの番号を入力します。 すると…… f:id:unifa_tech:20171027164946p:plain こんな具合に、会議に参加することが出来ます。 ここで特徴的なのが、画面中央の「Join by phone」です。前述の通り、この機能は何らかの理由でPCからの音声送信が出来ない環境向けに作られた機能なので、画面の指示通りに実行すると、電話から「こずえ」として参加することが出来ます。 キャプチャは「米国」になっていますが、プルダウンで「日本」を選択し、表示された電話番号に電話→英語のアナウンスのあと、ミーティングIDと自分に割り振られた参加者IDを入力で、参加可能です。

また、自分から画面共有をすることは出来ませんが、相手が共有してくれたものを見ることが出来ます。 f:id:unifa_tech:20171027165646p:plain

まとめ

今回は、Web会議システムのZoomの拡張機能でこんなことができる!という一例を見て頂きました。 本当はサンプルを動かすまでの手順や、裏側の仕組みも説明しようかと思っていましたが、あまりにも長くなる為断念。。。マイナー機能であるということもあり、今回はご紹介という形にとどめました。 他にも、Zoomはリアルタイム背景合成が出来たり、カメラ画像が美肌モードになったり(!)という面白機能がありますので、機会がありましたら是非使ってみてください。

それでは。

Reactで機械学習用データラベリングアプリを作る

こんにちは、田中です。

弊社サービス、るくみーでは保育士のみなさんやプロカメラマンが撮影した写真が日々たくさんアップロードされています。 定期的にこの子ども達の様子が保護者のみなさんに公開・販売されますが、販売が終了したあとの写真は基本的にデータサーバーで眠るのみです。。><

この膨大に蓄積されている園写真を機械学習の力で価値につなげていきたい... !

ということで、その最初の一歩 兼 React勉強の一環として機械学習用データラベリングアプリを作ってみました。 ƪ(•◡•ƪ)"

できたもの

こんな感じです。

f:id:sanshonoki:20171024062117g:plain

写真の枚数が大きいと見込まれるので右側の写真リストは無限スクロールとなるように実装しました。

一番下の写真までスクロールしたときにデータが読み込まれてブラウザのスクロールバーが伸びることが分かると思います。

無限スクロール

FacebookやTwitter、Instagramといった人気のソーシャルメディアでのコンテンツ表示のUIとして使われています。 ユーザが下へスクロールするにつれてページのコンテンツが継続的にロードされるというアレです。

無限スクロール以外の選択肢としてはページネーションがあり、それぞれ長所短所があります

上の記事は主にビジネスコンテンツ目線での長所短所ですが今回はキーボードのショートカット操作だけでも楽に作業できることを重視し無限スクロールを採用しました。 (「進む」「戻る」の操作だけでよい)

react-infinite-scrollerを使った無限スクロール

react-infinite-scroller というパッケージを使いました。 他にも何個かありましたがこれが最も分かりやすく使いやすかったです。

github.com

create-react-appで作ったアプリをもとに無限スクロールを実現するコードを紹介します。

一番シンプルな例

f:id:sanshonoki:20171020104602g:plain

App.js

import React, { Component } from 'react';
import './App.css';
import InfiniteScroll from 'react-infinite-scroller'

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      items: [],
      hasMoreItems: true
    }

    this.loadItems = this.loadItems.bind(this)
  }

  loadItems() {
    const current_item_count = this.state.items.length
    const max_items = 300
    const page_item_size = 50

    if (current_item_count < max_items) {
      const new_ids = Array.from(Array(page_item_size).keys()).map((num) => {return num+current_item_count})
      const new_items = new_ids.map((id) => {return {'id': id}})

      setTimeout(() => {
        this.setState({items: this.state.items.concat(new_items)})
      }, 500) // add some delay for demo purpose
    } else {
      this.setState({hasMoreItems: false})
    }
  }

  render() {
    const items = this.state.items.map((item) => {
      return <div>{item.id}</div>
    })

    return (
      <div className="App">
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>

        <InfiniteScroll
          pageStart={0}
          loadMore={this.loadItems}
          hasMore={this.state.hasMoreItems}
          loader={<div>Loading...</div>}
          initialLoad={true}
        >
          {items}
        </InfiniteScroll>
      </div>
    );
  }
}

export default App;
  • <InfiniteScroll></InfiniteScroll> でスクロールさせる要素を囲む
  • loadMoreで新しいデータを読み込み、配列に追加する
  • 読み込むデータがなくなったら hasMorefalse にする

基本これだけです。簡単ですね :-)

フッター固定の例

続いてフッターを固定で表示する場合です。

f:id:sanshonoki:20171020104628g:plain

App.css

html, body {
  height: 100%;
}

.App {
  text-align: center;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.scrollContainer {
  height: 100%;
  overflow: auto;
}

.footer {
  height: 30px;
  margin: 10px;
}

App.js

import React, { Component } from 'react';
import './App.css';
import InfiniteScroll from 'react-infinite-scroller'

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      items: [],
      hasMoreItems: true
    }

    this.loadItems = this.loadItems.bind(this)
  }

  loadItems() {
    const current_item_count = this.state.items.length
    const max_items = 300
    const page_item_size = 50

    if (current_item_count < max_items) {
      const new_ids = Array.from(Array(page_item_size).keys()).map((num) => {return num+current_item_count})
      const new_items = new_ids.map((id) => {return {'id': id}})

      setTimeout(() => {
        this.setState({items: this.state.items.concat(new_items)})
      }, 500) // add some delay for demo purpose
    } else {
      this.setState({hasMoreItems: false})
    }
  }

  render() {
    const items = this.state.items.map((item) => {
      return <div>{item.id}</div>
    })

    return (
      <div className="App">
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <div className="scrollContainer">
          <InfiniteScroll
            pageStart={0}
            loadMore={this.loadItems}
            hasMore={this.state.hasMoreItems}
            loader={<div>Loading...</div>}
            initialLoad={true}
            useWindow={false}
          >
            {items}
          </InfiniteScroll>
        </div>
        <div className="footer">Footer</div>
      </div>
    );
  }
}

export default App;

こちらはスタイルを少し工夫する必要があります。

  • ウインドウ高さをheight: 100vh;で固定し、 overflow:hidden でスクロールバーを見せない
  • <InfiniteScroll></InfiniteScroll> をラップする<div>要素を作り、height: 100%; overflow: auto; にする
  • useWindow={false}のパラメータを追加する

create-react-appで適当なアプリを作ってApp.jsとApp.cssを上書きすればすぐにできるので興味あれば試してみてください

おわりに

ラベリングツールは一応の形になりました!

が、これからチマチマとラベリング作業していくのかと思うとちょっと憂鬱。w

でも、頑張りたいと思います。。 (•̀ᴗ•́)و