ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

Vapor. Sharing the code between an iOS app and... the server?

By Vyacheslav Vorona, iOS developer at UniFa.

Being an iOS developer, have you ever been in a situation when you were thinking: "Alright, I want to write this kind of app. Now!" But then you instantly realize that your pet-project would also need some kind of backend and you are too busy/lazy to learn all that "server stuff"... so your enthusiasm disappears as quickly as it popped up five minutes ago. Happens to me all the time. 😅

Luckily, there are technology choices allowing you to write your backend code using the language you are used to. I looked through some of them and decided to give Vapor a try. IMHO, it is risky to use Vapor in any big and serious project right now, but for hobby programming... it is perfect. Today I am going to take a closer look at it and I hope you will join me (don't forget tea and cookies ☕️).

What are the goodies?

  • You use your favorite Swift for coding
  • You are doing it in your favorite Xcode with all it's debugging tools available
  • Cmd + R and your server is up locally
  • Type checking during the compilation
  • An ability to have code/models shared between your iOS app and the backend

The last point is especially intriguing and provides some opportunities you wouldn't have with a backend written with any commonly used server-side programming language. Just imagine: you change your server code and an iOS app is already aware of it. Depending on the case your changes are going to be handled automatically (and you may not even need to change anything in your client code) or your app's compilation is going to fail letting you know something is wrong. Sounds like an attractive opportunity to me. Unfortunately, there is almost no example code implementing such solutions to be found on the Internet. So the only way we have is to try to implement it by ourselves, which is what we are going to do today.

Sounds cool! What do I need?

  • Vapor requires you to have Xcode 9.3+ and Swift 4.1+ (for this particular project we will need Swift 5.1 though)
  • We will also need Homebrew to install Vapor

Optionally: I am going to use Postman client to test API calls, but you may use any tool you like (or not use any at all).

That's it. Let's dive into it!

Setting up the projects

Alright, first things first, since we are going to have two projects sharing some code, we need to create a workspace in Xcode. I am going to call it SchrodingerApp.workspace. Got it? Schrodinger's cat is both dead and alive at the same time, and our code will be in the iOS app and the server at once... ah, never mind. 🙄

Next, let's download and install Vapor. Let me copy and paste some console commands from it's 'Hello, world' page for you...

Installing Vapor using Homebrew:

brew tap vapor/tap
brew install vapor/tap/vapor

Easy, right? Then I will run the command below in the same folder where I've put my workspace file.

vapor new SchrodingerAppServer

It will create a new folder with the name we entered, but if you look inside, you will notice it doesn't have an .xcodeproj file we are used to. Don't worry, Vapor can generate it for you:

cd SchrodingerAppServer
vapor xcode

When everything is done, it will suggest you to open Xcode, but we still need to put our new project into the workspace, so let's refuse for now. Open SchrodingerApp.workspace in Xcode and just drag-and-drop SchrodingerAppServer.xcodeproj from Finder to Xcode.

You may notice that now you have a scheme called 'Run'. Also, make sure you have your Mac selected as a device to build. Go on and run it! (just Cmd + R, as promised)

f:id:unifa_tech:20191125183937p:plain

In the Xcode console we see the message which says that our brand new server is up and waiting.

Server starting on http://localhost:8080

Since iOS Simulator uses the same network as the Mac it's beeing ran at, I will use the simulator to check the localhost. So, does our server work?

f:id:unifa_tech:20191125184059p:plain

Great!

We will take a closer look at the contents of our server app in a moment. But first, let's set up our client app to finish the boring part.

I am creating a new Single View iOS project named SchrodingerAppClient and pulling it's .xcodeproj file into our workspace. Make sure you have two schemes: one for the server and one for the client.

For the next step let's designate a place to put our shared code to.

Sharing is Caring

I consider the server app as a primary one, which means the actual files containing the shared code are going to be stored inside of it's project.

To do so I'm going to create a folder called SharedAPI ("New Group" in the Xcode) in SchrodingerAppServer/Sources/App. It will contain the code common for both of our apps.

Now I will go to the client project and add a "New Group without Folder" named SharedAPI into SchrodingerAppClient. This group is going to hold a reference to the SharedAPI folder we previously created for the server app. To add the reference select the newly created group, in the "File Inspector" click a tiny folder icon and search for a SharedAPI folder in the SchrodingerAppServer.

f:id:unifa_tech:20191125185426p:plain

Finally, we are ready to write some code! Yay! 🚀

Schrodinger's Model

I feel like our SchrodingerApp is missing something... A Cat data model, of course! I am adding a Cat.swift file into the SchrodingerAppServer/Sources/App/SharedAPI and here is a code for it:

final class Cat {
    var id: Int?
    var name: String
    var age: Int
    private var breedString: String

    var breed: Breed? {
        return Breed(rawValue: breedString)
    }

    init(id: Int? = nil, name: String, age: Int = 0, breed: Breed) {
        self.id = id
        self.name = name
        self.age = age
        self.breedString = breed.rawValue
    }
}

enum  Breed: String {
    case  persian = "persian"
    case  russianBlue = "russian blue"
    case  britishShorthair = "british shorthair"
}

I guess it is pretty straightforward for now, just a couple of things to note:

  • var id: Int? is in fact a requirement of the SQLiteModel protocol which I'm going to talk about in a second
  • I've declared private var breedString: String to see how Vapor is handling private properties

Getting Fluent

Vapor provides an Object-relational mapping framework called Fluent to work with databases. It supports several database drivers:

  • PostgreSQL
  • MySQL
  • SQLite
  • MongoDB

For our project, I decided to try out SQLite as it is simple and is just nice for prototyping.

To make Cat a full-fledged server-side model we have to adopt several protocols:

  • SQLiteModel. Allows your class to represent a table in an SQLite database. Its only requirement is to have var id: Int? which we already implemented in our model.
  • Migration. Allows us to perform dynamic migrations of our model (or revert them).
  • Content. Makes a model convertible to content of an HTTP message.
  • Parameter. Makes a model capable of being used as a Restful route parameter.

Latter three protocols do not have any requirements by default, we just need to declare the conformance.

But here is a thing: to do so, we need to import Vapor and import FluentSQLite which we obviously can't do on the iOS side. To make Cat model accessible from both server and client code, I'm going to use a small trick added in Swift 5.1. - canImport(). Add the code below right after the enum Breed in Cat.swift:

#if  canImport(Vapor) && canImport(FluentSQLite)

import FluentSQLite
import Vapor

extension Cat: SQLiteModel {}
extension Cat: Migration {}
extension Cat: Content {}
extension Cat: Parameter {}

#endif

This way Cat will conform to these four protocols only in case when Vapor and FluentSQLite are able to be imported. This means our data model is going to be treated differently: as an SQLite model on the server-side and just as a simple object on the client-side.

To make sure everything works I'm going to add a reference to Cat.swift into the SchrodingerAppClient and try running both schemas ("Run" for the server and "SchrodingerAppClient" for the client). If everything is set up correctly, both should compile successfully.

Two things left to do:

  • Add the Cat model to a MigrationConfig which is a Fluent structure managing database migrations in Fluent. To do it I will add the following line into the configure function located in configure.swift, right before services.register(migrations).
migrations.add(model: Cat.self, database: .sqlite)
  • (Optional) Right now the project we created doesn't persist any data between two launches of the server. To make it do so, we need to replace the this line in configure.swift
let sqlite = try SQLiteDatabase(storage: .memory)

with this

let sqlite = try SQLiteDatabase(storage: .file(path: "db.sqlite"))

So that the database is stored in a file.

We are done with the model. Now let's teach our apps to do something with it.

POSTing Cats

Server

In this project, we will implement an endpoint for our iOS app to POST new Cat models to the server via REST API. With Vapor it is really simple. Just find a file named routes.swift and add the code below to the bottom of the routes function located there:

router.post("api", "cat") { request -> Future<Cat> in
    return try request.content.decode(Cat.self).flatMap(to: Cat.self) { cat in
        return cat.save(on: request)
    }
}

Let's take a closer look at this code and see what it actually does.

router.post("api", "cat") registers a new route at the path /api/cat using the POST method. When a request is received, it's JSON gets decoded into Cat using Content protocol. Since decode(_:) returns a Future we need flatMap(to:) to unwrap a Cat when it is decoded. Then a Cat gets saved into the database using Fluent.

Now I`m going to run the server app and try out our new endpoint using Postman:

  • The URL is: http://localhost:8080/api/cat
  • My request's JSON is:
{
    "name": "Murka",
    "age": 5,
    "breedString": "russian blue"
}
  • Make sure to set Content-Type header to application/json

And here is the server response:

{
    "age": 5,
    "id": 1,
    "breedString": "russian blue",
    "name": "Murka"
}

As you can the breedString was handled properly (we made it private in the model, remember?). Also, note that Fluent have set an id for our freshly POSTed Cat.

Client

To POST some Cats from the iOS app I'm adding the method below right to the default ViewController.swift in SchrodingerAppClient:

private func post(_ cat: Cat) {
    let jsonData = try? JSONSerialization.data(withJSONObject: cat.json)
    var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!)
    request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
    request.httpMethod = "POST"
    request.httpBody = jsonData

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, error == nil else { return }
        let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
        if let responseJSON = responseJSON as? [String: Any] {
            print(responseJSON)
        }
    }
    task.resume()
}

I will not describe what's going on here in detail as I believe you understand it if you are reading this article. 😉 Just a couple of things:

  • Note that I am setting application/json as a value for Content-Type
  • Cat.json is just a simple mapping I've added to the model. It is a computed property of type [String: Any] representing a Cat instance to use in an API call

GETing Cats

Server

Next, I am going to add a GET endpoint which is going to be called by an iOS app to present a list of the Cats we've put into our Schrodinger's box. Again, with Vapor it us quite straightforward:

router.get("api", "cat") { request -> Future<[Cat]> in
    return Cat.query(on: request).all()
}

When the server receives a GET request at the /api/cat, it just queries all the Cats from the database and provides them to the client.

Testing new endpoint using Postman, here is the response:

[
    {
        "age": 5,
        "id": 1,
        "breedString": "russian blue",
        "name": "Murka"
    }
]

As you can see, we received an array containing one element - the Cat we added using POST endpoint.

Client

Here is some ugly code from the client-side to get a list of the cats for presentation. I've added it as a separate method into the ViewController.swift:

private func getCats() {
    var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!)
    request.httpMethod = "GET"
    let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error **in**
        guard let data = data, error == nil else { return }
        let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
        if let responseJSON = responseJSON as? [[String: Any]] {
            self?.cats = []
            for catJson in responseJSON {
                guard let cat = Cat.make(from: catJson) else { continue }
                self?.cats.append(cat)
            }
        }
    }
    task.resume()
}

The only thing worth explaining here is Cat.make(from:) method. It is just a static function decorating the initialization of Cat and able to return nil if some necessary data is missing.

Final preparations

User Interface I am adding some simple UI to test the features we implemented:

  • Three UITextFields to add new Cats via POST (yeah, there should be pickers or something similar, but this project not going to the AppStore, alright? 😅)
  • UITableView to display the Cats the client recieves via GET

Info.plist Another important thing to do is to modify Info.plist in SchrodingerAppClient as follows:

f:id:unifa_tech:20191125185650p:plain

We need to set NSAllowsArbitraryLoads key under the NSAppTransportSecurity dictionary to YES. Otherwise your iOS app will not be able to connect to the localhost.

Note that this is an anti-pattern and you should never do it in a real project. Apple is strongly against doing it and your app may be rejected from the AppStore.

But it's ok now, since we are just playing around. 😎

Now let's run our app and look at the results, even though they don't look as exciting as some mechanisms we implemented under the hood:

We can input some data for the new Cats, POST it... f:id:unifa_tech:20191125185756p:plain

... and then GET the Cats to see all of them in the list: f:id:unifa_tech:20191125185806p:plain

And all of it is done using a single Cat data model! 🎉

Conclusion

Let's be honest, I doubt Vapor (and backend Swift overall) is going to become a somewhat serious competitor for all those languages used for server-side development any soon. Yes, Swift is incredibly fast, has strict types and a bunch of other advantages, but it needs something more than just support from its community (even though they are awesome guys) to become a "real thing" .

BUT! Does it mean you shouldn't use it for your cute little pet-project you are writing every weekend with a beer in your hand? No! You definitely should. Finally, you can write everything from A to Z without leaving your comfort zone.

Besides, today we've learned a bit about some opportunities which mobile platform developers don't usually have. With Vapor you don't have to feel that you are writing bad code just to fit some weird architecture on the server-side. Your backend code influences your iOS app directly. And if you need to add something to the client, the server code is already aware of it. It's a great feeling, especially if you don't have much experience with backend.

I hope I could inspire you to give Vapor a try.

Happy coding! 😉

Project repo (make sure to check README)

iPodカメラによる顔検出

はじめに

iOSエンジニアをやっていますわたなべと申します。 現在私は、フォト事業部の方で自動撮影アプリというアプリの開発に携わっています。 自動撮影アプリというのは、保育園で保育士さんがBluetoothカメラとiPodを使って園児の様子を自動で撮影するアプリになります。いろいろと課題があるので、下記のようなことを考えてみました。

やりたい事

iPodには、カメラが付いているので、そのカメラを使って撮影できないか? という訳で、iPodの背面カメラを使用し、カメラ内に顔が検出できたら、数秒間、動画データから全てのフレームを画像として保存する。ということをやりたいと思います。

カメラを使用する

今まで、カメラを使用したアプリを作った事がなかったので、ここから軽く説明していこうと思います。 カメラを使用するにはAVFoundationをインポートします。 そして、入力と出力を管理するAVCaptureSessionを生成します。 まず、入力はAVCaptureDeviceを用いて、使用するカメラを設定します。(今回は背面カメラを使用して、フレームレートを1/30秒に設定しています。) デバイスの設定が終わったら、これを入力デバイスとしてAVCaptureSessionに追加します。 続いて、出力です。出力は、カメラの入力を動画データとして扱いたいので、AVCaptureVideoDataOutputを使用します。 AVCaptureVideoDataOutputでは、ビデオ出力などを設定し、setSampleBufferDelegate(〜)を設定すると AVCaptureVideoDataOutputSampleBufferDelegateプロトコルで、メソッドcaptureOutput(〜)がフレーム毎に呼ばれますので、ここで顔検出を行います。(1フレームづつ画像に変換して顔検出を行います。) こちらも設定が終わったらAVCaptureSessionに追加します。

そして、キャプチャした映像をプレビュー表示するために、AVCaptureVideoPreviewLayerを設定して、

キャプチャースタートです!

最後に、Info.plistにカメラを使用する事を忘れずに…

顔検出方法

顔の検出方法は、iOS11から搭載されているVisionフレームワークを使用します。

Vision.フレームワークでは、いろいろな検出ができるので、 まず、どのような検出を行うかを要求して、(Request) 何に対して、検出処理をするか指定して実行、(RequestHandler) そして、検出結果を処理します。(Observation)

そこで、Visionで顔検出を行う際に重要になってくるクラスがあります。 今回は、顔を検出したいので、下記のクラスを使用しています。

役割 クラス 説明
Request VNDetectFaceRectanglesRequest 顔検出を要求します。
RequestHandler VNImageRequestHandler 上記のRequestに対する検出処理を実行するクラスです。
Observation VNFaceObservation Requestに対する検出結果を処理をするクラスです。

下記が実際のコードになります。(顔検出部分を抜粋)

 1: let faceRectanglesRequest = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
 2:     for observation in request.results as! [VNFaceObservation] {
 3:         /// observationには検出した顔の矩形が入ってくる
 4:         /// 検出できた顔の部分に矩形などを表示する処理を行う
 5:     }
 6: })
 7:
 8: if let cgImage = image.cgImage {
 9:     let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
10:     try? handler.perform([faceRectanglesRequest])
11: }
  1. 1行目でどのようなRequestを要求するか指定しRequestを作成します。今回は、顔検出なので、VNDetectFaceRectanglesRequestを使います。
  2. ちょっと飛んで9行目から説明します。検出したい画像を指定しVNImageRequestHandlerを作成します。  ここでの解析したい画像は後ほど説明します。
  3. 10行目でVNDetectFaceRectanglesRequestで生成した要求に対して顔検出を開始します。
  4. 検出した結果が、2行目のrequest.resultsに入ってきます。

上記の処理を「カメラを使用する」でも説明したcaptureOutput(〜)内で処理を行っています。 captureOutput(〜)の最初で1フレームづつ画像に変換し8,9行目で使用しているimageがそれになり、検出を実行しています。

最後に

当初の目的の顔検出ができた時、そのあと数秒間のフレームを画像に書き出すという処理を追加しているので、目的は達成できたかとは思うのですが、まだまだ、改良の余地ありですね。

今回は、駆け足で説明してしまったので細かい所、顔を検出した所に枠を出すとか、検出した結果の顔の座標は反転しているとか、いろいろと注意しなければいけない点が多々ありますが、今後の課題としていろいろと試していければと思っています。

2019年のプロジェクトをふりかえる 〜そのとき何があったのか、何をしたのか〜

スクラムマスターの渡部です!

はじめに、この記事はUniFa Advent Calendar 2019の記事でもあり、ふりかえり Advent Calendar 2019の記事でもあります。

今回の記事では、12月ということもありますので、この1年で関わったとあるプロジェクトについてふりかえってみたいと思います。

目次
  • はじめに
  • 走り出しフェーズ
  • 予測し難いことこの上ないぞ!?フェーズ
  • 徐々に安定してきた!チームをもっと強くするぞ!フェーズ
  • 一気にベロシティ低下したぞ!フェーズ
  • ラストスパートフェーズ
  • 2020年に向けて一言
  • さいごに

はじめに

ふりかえるにあたって、対象のプロジェクトの全体像、説明で用いるグラフの見かたを軽くご紹介させていただきます。

※ 記載されている数値は全て実際のものです。

バーンダウンチャート

f:id:unifa_tech:20191206163513p:plain

<グラフの見かた>

  • 赤い線:開始日から理想の完了日まで、均等にタスクを消化した場合の残りポイントの推移
  • 青い線:実際に消化されたポイントを差し引いた残りポイントの推移

チームでは相対見積もり(ストーリーポイント)を採用しています。

一見すると、比較的安定した下降線を描いているように見えますが、要注意なのは、トータルの見積もりが500ポイントある(つまり、傾きの変化が僅かに見えても、実際はけっこう差があるぞ)ということです。

そこで、スプリントごとにどれだけのポイントを消化できたかを比較できるグラフも用意していますので、合わせて確認していければと思います。

ベロシティの推移

f:id:unifa_tech:20191206163550p:plain

<グラフの見かた>

  • 赤い線:プロジェクト遂行とは直接は関係ない、調査 / 不具合 / 採用などの作業のポイント(以降、プロジェクト外作業と呼称)*1
  • 青い線:プロジェクト遂行に関わる全ての作業のポイント(以降、プロジェクト内作業と呼称)

おや?先ほどの印象とは打って変わって、かなりファンキーな印象を受けますね。

ではさっそく、フェーズごとにふりかえっていきたいと思います。 (おこったこと一つ一つに深堀りしていくと薄い本が出せる気がするので、今回は深堀りません)

走り出しフェーズ

f:id:unifa_tech:20191206165515p:plainf:id:unifa_tech:20191206165518p:plain

一見、ベロシティは安定して右肩上がりのように見えます。 が、後述しますが、実測値ではないので、このフェーズの数値は強くは意識しないでください。

このフェーズでは(私が関わったのはスプリント4からですが)、何よりもまず優先で、チームの状況を見える化することにフォーカスしました。

やったこと、おこったこと
  • 祝 join!@スプリント4(懐かしい!)
  • 見積もりを取得した(プランニングポーカーの実施)
  • ストーリーポイント見積もりの説明・導入した
  • スプリント毎のベロシティ計測を実施しはじめた
  • バーンダウンチャートを使って、全ての関係者へ状況を見える化した
  • スプリント4以前はベロシティを計測できていなかったので、後日ヒアリングを元に工数を仮置きした(翌スプリント以降のばらつきを考えると、実際計測してたらこんななだらかな線にはならないかも)

予測し難いことこの上ないぞ!?フェーズ

f:id:unifa_tech:20191206165656p:plainf:id:unifa_tech:20191206165659p:plain

バーンダウンチャートでは比較的なだらかに見えますが、ベロシティの推移を見てみると突然の乱高下です。荒ぶっていますね。

このような状況ですと、次のスプリントでどの程度コミットするべきかの予想を立てにくく、不明確なコミットの上に成り立つその先の計画も不明確なものになりかねません。(当時は、昨日の天気*2もブレが大きすぎて参考に出来ない状況でした)

POや関係者とは、「いまの段階で着地の予想は出来なくはないがブレが大きいはずなので、リリース日の決定はまだ待つべきだろう」という話をしていたことを覚えています。

スプリント4まではカイゼンのための土台作りに100%フォーカスしており、上記の状況が明らかになりましたので、スプリント5,6から実際にアクションを実施していきました。

このフェーズでは、透明性の確保にも引き続き勤しみつつも、判明した障害の中でも重要なものから順番に取り除いていました。

やったこと、おこったこと
  • スプリント毎のベロシティが安定しなかった(おや?こいつぁ予測が困難だぞ!?)
  • ありとあらゆるプロジェクト外作業を全てチケット作成し、見積もりを取得しはじめた
  • 結果、プロジェクト外作業が頻繁に発生し、集中できる時間が作れていないことが判明した
  • プロジェクト外作業が発生するプロセスを把握した後、フローを整理して窓口を一本化した(エンジニア直ではなく、一先ず私へくるように)
  • プロジェクト外作業に対して、専任スタッフを設けた(これは非常に効果が高かった)*3
  • 仕様変更/追加が判明したとき、進行中のチケットには含めず、別でチケットを作成することにして、仕様追加/変更によるスケジュールへの影響を可視化した
  • 絶対に必要ではないMTGはキャンセルしてもらい、必要なMTGは時間を朝or夜にずらして、一日の真ん中は可能な限り集中できる時間にした

徐々に安定してきた!チームをもっと強くするぞ!フェーズ

f:id:unifa_tech:20191206165709p:plainf:id:unifa_tech:20191206165712p:plain

チームみんな・関係者全員の協力と、努力の甲斐あって、徐々にベロシティが安定してきました。

ただ、大きな障害は取り除いたものの、まだまだスムーズに作業が進まないことがあります。

最低限の透明性は確保されてきておりましたので、このフェーズでは、日々の作業がもっとスムーズになるように、あれこれと試行錯誤をしていました。

因みに、いまの私のアイデンティティの一部を担っていると言っても過言ではない「ふりかえり」に関心を抱いたのも、このフェーズでのことでした。

やったこと、おこったこと
  • 見積もりが大きいチケットは、だいたい5pt上限で分割するルールにしてみた。結果、チケットごとに何をすべきかが明確になり認識ズレが減り、翌スプリントへの持ち越しも減った
  • 仕様書の記載方法を変更した。具体的には、1画面につきコンフルエンス1ページを用意して仕様は箇条書きで記載し、そのページをみれば、決まっていることの全てが分かる状態にした
  • これまでは実装しながら仕様をFixさせていっていたが、着手するスプリントの前には仕様がFixされている状態にした(前々から準備してはいたが、このタイミングで仕様の整理がようやく追いついた)
  • PO確認待ちで作業が止まったり、複雑な説明をテキスト入力することに時間がかかっていることが判明したので、POを説得してチームの隣に席移動してもらい、いつでも確認相談して良いルールにした(これもかなり助かりました)
  • 仕様に関する質問は、コンフルエンスの該当仕様書の下部に残すルールにした(後から決定の背景を追いやすくて地味に好評。Slackだと流れて後で確認に手間がかかる)
  • アジャイルレトロスペクティブズを参考に、「ふりかえり」の抜本的改革を実施(場の設定 > データの収集 > アイデア探し > アクションの決定 > ふりかえりのふりかえり > ふりかえりの終了 という流れに組み替えた)
  • Twitterで @viva_tweet_x さんを発見、衝撃とともにフォロー & ふりかえり読本を購入
  • ワーキングアグリーメントを定めた。個人的なお気に入りは「作業報告があったら、「Great!!」と行動を称賛する」
  • ふりかえりでBGMを流した。個人的なお気に入りは 楽ジャズ~クラシック / Easy Camel Trio

一気にベロシティ低下したぞ!フェーズ

f:id:unifa_tech:20191206165721p:plainf:id:unifa_tech:20191206165725p:plain

スプリント12からプロジェクト外作業が増え始めてはいましたが、ここで一気にチームのベロシティが低下しました。

が、その後の状況の正確な把握から、PO・関係者含めて検討し、スピーディに対策を打つことができたフェーズでした。

いま思えば、関係者全員の協力による「透明性の確保」、「ふりかえりによるチームの強化・情報の共有の機会」が無ければ、ここまで迅速な対応は出来なかったのではないかなぁと思います。

やったこと、おこったこと
  • QAチームによるテストがスタート
  • テストに関連した質問、バッチの手動実行、データ作成作業などが発生し、ベロシティが急激に低下していたことが判明
  • QAチームとのコミュニケーションを最適化
  • 何でも確認相談できる枠を、朝イチに設けた
  • Bugチケットを簡単に管理するための専用のカンバンをJIRAに用意
  • Bug発見時の確認プロセス、判断のガイドラインを整備し、確認すべきものだけを適切に確認できるようにした
  • 元々の開発チームの作業をよく理解しているエンジニア2名(同じチームではあるが、同時並行で別作業をしてもらっていた)にjoinしてもらう計画をたてた

ラストスパートフェーズ

f:id:unifa_tech:20191206165734p:plainf:id:unifa_tech:20191206165737p:plain

これ以降は、リリースに向けてやる必要があることをとにかくなんでもやっていました。

(テキストに起こすとちょっと恥ずかしい気もしますが…)関係者全員が一致団結しているなぁと強く感じたフェーズでもありました。

やったこと、おこったこと
  • 私、PO、他ビジネスサイド関係者がテストに参加(主に修正確認など)
  • 前フェーズで述べた2名のエンジニアが実際に作業開始
  • POとウィークリー(最後はデイリー)で、Bug優先度チェックを実施。「修正されると嬉しい」レベルのBugは全てPendingした
  • 日々の運用作業も、緊急のものを除き、対応をストップした(関係者の皆様のご理解に感謝)

因みに、上記のような大きな変更を、弊社では「オペレーション One UniFa」と勝手に呼称していました。

そして、開発チーム、QAチーム、PO、デザインチーム、ビジネスメンバー、他関係者全員の協力のお陰で、世の中に価値を届けることができました。

2020年にむけて一言

2019年は、成功失敗、良かったこと悪かったこと問わず、非常にインプットに重きを置いた一年であったように思います。(後半は徐々にアウトプットしはじめましたが)

ですので2020年は、これまで通りインプットしつつも、よりアウトプット(社内外問わず)に重きをおいて活動していく所存です。

ひとまず、社内の「スクラム勉強会」「ふりかえり導入支援」は計画的に実施していきたいと思います。

さいごに

ふりかえってみると、本当に様々なことをやってきたなぁとしみじみ思います。

いま思い返せば「あの時こうしていたら…」という気持ちがよぎらないことも無いですが、そんなときこそノーム・カースの最優先条項を読み上げて次に活かすとし、2019年を気持ちよく終えたいと思います。

私たちが発見したものに関係なく、その時点で知っていたこと、スキルと能力、利用可能なリソース、手元の状況を考えれば、誰もが最高の仕事をしたと理解し、本当に信じています。*4

「いうても自分ら、よく頑張ったさ」と。

それでは引き続き、アドベントカレンダーをお楽しみください!

*1:グラフにどのような数字を含めるべきかについてはこちらの記事で解説していますので、よければご覧ください。tech.unifa-e.com

*2:過去3スプリントのベロシティの平均を、次スプリント時の計画の参考にします。それを「昨日の天気を見る」と言います。

*3:専任スタッフに関しては別記事でも解説しています。tech.unifa-e.com

*4:このテキストは、次の原文をGoogle翻訳したものです。The Prime Directive - Agile Retrospective Resource Wiki

ルクミーフォトリニューアルにおけるデザインワーク

5月に入社したデザイナーのYogaif:id:unifa_tech:20191209181205p:plainです。

この↑アイコンはプライベートの友人が描いてくれました:) 似ているらしい…

今日はルクミーフォトのリニューアルプロジェクトについて、デザイナー視点でざっくり書いてみようと思います。

  • ユニファのデザイナーってどんなことをしているの?
  • 事業会社のインハウスデザイナーってどんなことをしているの?

というようなことに興味のある方はおつきあいください:)

今回の記事で書いていること

  • ルクミーフォトについて
  • リニューアルプロジェクトの進み方とデザイナーの役割
  • 実際の管理画面

ルクミーフォトについて

ルクミーフォトはIoTを使った保育施設向けのフォトサービスです。

プロダクトとしては大きく分けて2つあり、社内ではそれぞれ園管理画面・保護者画面と呼んでいます。

園管理画面
園管理画面:保育園職員が、撮影した写真をアップロードしたり販売設定したりするWebサイト。PCブラウザ

保護者画面
保護者画面:園児の保護者が、園で撮影された写真を閲覧したり購入したりするWebサイト。PCブラウザ・スマートフォンブラウザ

この両方を2018年〜2019年にかけてフルリニューアルしました。

続きを読む

Swift-Jupyter をつかってみました

はじめに

こんにちは、iOSエンジニアのしだです。UniFa Advent Calendarで、普段の開発であまり関わりの少ないメンバーの記事を見てにやにやしてます 😏

こちらは、UniFa Advent Calendar 2019 の12日目の記事になります。

もう2019年が終わりますが、今年は、TensorFlow Dev Sumit 2019があったり、TensorFlow 2.0になったり、O'Reilly主催のTF World '19があったりTensorFlowのアップデートが多かった気がします。 その中で、以下の Swift for TensorFlow (TF World '19) の動画を見ていたところ、何やら Jupyter notebook に Swift を書いていてなんじゃこりゃと思って調べてみたらSwift-Jupyterというものを知ったので使ってみます。

続きを読む

The Pi with Eyes

By Matthew Millar Research Scientist at ユニファ

Purpose:

This blog will cover the progress of an IoT project for computer vision. The final goal will be to locate an individual and to track this individual to see where they are in a room and to tell if they go near a marked area in the room. This will involve; person detection, tracking, and distance measurements from a Raspberry Pi.
Last time we set up the Pi and got things ready to go.
My 2nd Favorite Pi - ユニファ開発者ブログ

This blog post will look at setting up the Pi so it can stream the video feed from a camera. We will cover how to install OpenCV on the Pi and set up a synchronized camera feed from a special Camera Hat which will be discussed later!

IoT:

The IoT is mainly about connecting everything around us to make a holistic data acquisition system. This means all the data that you create is gathered by sensors and sent to the mother node! Let’s break down IoT.
In a nutshell, IoT is a collection of Node or edge devices that gather data by sampling the world around them. This data can range to a large amount because of the different data that can be collected. It can be temperature and weather data, camera feeds, IR sensors, tablets, refrigerators and appliances, and wearable devices to name a few. This is not a comprehensive list as this list is endless and getting longer every day.

Cameras are one of the most common methods for gathering data in an environment. They can be considered the first edge device that was implemented. The world's first security camera came from a chicken farmer who wanted to see who stole his eggs back in 1933. So, the farmer rigged up a camera (like that 35mm film camera professional (use) and hipster have) and set a trap so when the door to the chicken hut was opened the camera would take a picture. He even went so far to put a tin can to make a noise and have the thief look up at the camera [1]. This was the first recorded instance of a security camera. (Yes, the thief was captured and was found guilty).

OpenCV on Pi:

We will need at least one library for using cameras on the Raspberry Pi. That one will be the most common one to use in computer vision projects, OpenCV. This can be complex to install as the older method

pip install opencv-contrib-python

It does not work well with Opencv4 on some Pis. (I tried and failed). So I had to install it from the source. We will go through the steps one by one to get started.
First things first let us get things ready for OpenCV
Update and upgrade the OS first (just like Linux)

sudo apt-get update && sudo apt-get upgrade

Next, we need cmake installed to build the files later.

sudo apt-get install build-essential cmake pkg-config

And then the I/O packages for images

sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng-dev

And I/O packages for videos

sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install libxvidcore-dev libx264-dev

We need the GTX development libraries for OpenCV so let’s get them too

sudo apt-get install libfontconfig1-dev libcairo2-dev
sudo apt-get install libgdk-pixbuf2.0-dev libpango1.0-dev
sudo apt-get install libgtk2.0-dev libgtk-3-dev

And a library for aiding in Matrix Manipulation

sudo apt-get install libatlas-base-dev gfortran

And finally the HDF5 and QT GUI

sudo apt-get install libhdf5-dev libhdf5-serial-dev libhdf5-103
sudo apt-get install libqtgui4 libqtwebkit4 libqt4-test python3-pyqt5

Now with all that installed and ready to go let's make our environment to use OpenCV in

Step 1 get and install pip (if you don’t have it already)

wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py
sudo python3 get-pip.py
sudo rm -rf ~/.cache/pip

Now the virtual environment manager

sudo pip install virtualenv virtualenvwrapper

We will update the bashrc file now with the needed information

vim ~/.bashrc

and then add this to the bottom of the file (adjust as needed but if you followed the above exactly then there should be no adjustments)

# virtualenv and virtualenvwrapper
export WORKON_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
source /usr/local/bin/virtualenvwrapper.sh

Exit out of the bashrc file and run source to apply all the changes to the bashrc file

source ~/.bashrc

Finally, we can create a new virtual environment

mkvirtualenv myenv_name -p python3

Activate the environment using

Source /path/to/env/myenv_name/bion/activate

And install some needed dependencies.

pip install "picamera[array]"
pip install numpy

Now we can finally get to installing OpenCV from the source

cd ~
wget -O opencv.zip https://github.com/opencv/opencv/archive/4.1.1.zip
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.1.1.zip
unzip opencv.zip
unzip opencv_contrib.zip
mv opencv-4.1.1 opencv
mv opencv_contrib-4.1.1 opencv_contrib

This will get all the code you will need to build and install OpenCV
The next step is to make the swap file larger as if you don’t you will not be able to install OpenCV as it takes up too much room

sudo vim /etc/dphys-swapfile

and change the swap size from 100 to 2048. But later on, you will change it back to 100 as using 2048 can burn out your SD card quickly.

# set size to absolute value, leaving empty (default) then uses computed value
#   you most likely don't want this, unless you have an special disk situation
# CONF_SWAPSIZE=100
CONF_SWAPSIZE=2048

After the install, you will change it back to 100 Stop and start the swap service

sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start

Now go back to your virtual environment to work on it. And then we will start to build and configure OpenCV

cd ~/opencv
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
    -D ENABLE_NEON=ON \
    -D ENABLE_VFPV3=ON \
    -D BUILD_TESTS=OFF \
    -D INSTALL_PYTHON_EXAMPLES=OFF \
    -D OPENCV_ENABLE_NONFREE=ON \
    -D CMAKE_SHARED_LINKER_FLAGS=-latomic \
    -D BUILD_EXAMPLES=OFF ..

What makes this special is that ENABLE_NEON is on so that OpenCV is optimized for the ARM processor.
Now you can run

cmake 

But make sure you are inside the build file use pwd to make sure it is in the directory opencv/build/
Next step after the build is done using the

make -j4

Command to compile the code. The -j4 will make it faster as it will use all 4 cores of the Pi. You can leave off the -j4 to avoid possible race conditions if it freezes during install. This will take a hot minute so get lunch and possibly dinner and come back.
Now you can run

sudo make install
sudo ldconfig

The next step is to change the swap size back in the above steps.

Now we need to make a sym-link from the Opencv to python.

cd /usr/local/lib/python3.7/site-packages/cv2/python-3.7
sudo mv cv2.cpython-37m-arm-linux-gnueabihf.so cv2.so
cd ~/.virtualenvs/cv/lib/python3.7/site-packages/
ln -s /usr/local/lib/python3.7/site-packages/cv2/python-3.7/cv2.so cv2.so

Now if all that worked then you can check to see if it actually worked or not (took me 3 tries so don’t get disheartened).
Activate your virtual environment and run the following commands

Python
>>>Import cv2
>>>Cv2.__version__

The Camera Man:

Now we can turn our attention to building the camera system for the project. Looking at the final goal of the project of distance estimation we will need a synchronized set of cameras. Using two cameras that are calibrated and synchronized can greatly improve the accuracy of distance/depth predictions [2]. The camera kit that I will be using will be the Arducam 5MP Synchronized Stereo Camera Bundle Kit for Raspberry Pi that can be bought from Uctronics [3]. Using this chip will handle all the synchronization issues that come with dual camera feeds.

The Setup:

The setup is pretty straight forward. The first step is to connect the ribbon cable from the ArduCam chip to the camera input on the Pi.
f:id:unifa_tech:20191129113459j:plain
f:id:unifa_tech:20191129113515j:plain
Then place the pins into the right connectors on the HAT this will be the topmost pins on the Pi.
f:id:unifa_tech:20191129113532j:plain
And that's it you are set up and ready to begin installing more packages and dependencies! Let's plug all out stuff in the pi and your setup should look something like this
f:id:unifa_tech:20191129113643j:plain
Now even MORE setup and setting changes!
The first step is to enable the camera. On the Pi Desktop go to

Preferences->Raspberry Pi Configuration->Interfaces.

Then Enable the Camera, SSH, VNC, and I2C by clicking the enable radio button. The next step is to download the SKD for the ArduCam chip from GitHub

git clone https://github.com/ArduCAM/MIPI_Camera.git

Go into the RPI folder in the repo and run this command

chmod +x ./enable_i2c_vc.sh
./enable_i2c_vc.sh

This will enable the i2c_vc.
Guess what we need even more Packages again! So run these commands to install them both

sudo apt-get update && sudo apt-get install libzbar-dev libopencv-dev

This will set you up well so you are finally finished installing packages.
The next step is to make the install the code from the repos so do this

cd MIPI_Camera/RPI
make install

Next, compile the examples for testing

make clean && make

And finally, run it in preview mode (a C program from the chips creators).

./preview_setMode 0

To actually test out your code. If everything worked out ok you should start to see an image being streamed from the device to your Desktop like this
f:id:unifa_tech:20191129113834j:plain
or this
f:id:unifa_tech:20191129113857j:plain

And there you have it you now have an RPi 3B+ with a stereo camera that is synchronized and ready for the next steps.
Next time I will install TensorFlow and start with a simple object detection AI.

Sources:

[1] https://innovativesecurity.com/the-worlds-first-security-camera/

[2] Peleg, S., & Ben-Ezra, M. (n.d.). Stereo panorama with a single camera. Proceedings. 1999 IEEE Computer Society Conference on Computer Vision and Pattern Recognition (Cat. No PR00149). doi: 10.1109/cvpr.1999.786969

[3] https://www.uctronics.com/index.php/arducam-synchronized-stereo-camera-bundle-kit-5mp-for-raspberry-pi-2173.html

ビーコン信号による位置推定(入門)

研究開発部の浅野です。ユニファ2019年アドベントカレンダー10日目の記事は、BLEビーコンを使った位置推定について書きたいと思います。無線信号を活用して位置情報を取得するための方法には様々なアプローチがあります。今回はその中で最もシンプルでわかりやすいビーコン信号強度を利用する方法を紹介します。


構成

ビーコンを活用したサービスは、インドアナビゲーションや近接マーケティングなどのようにビーコン信号を送信(アドバタイズ)する機器が固定されていて移動する人が持つスマホなどでそれを受信する形で応用されていることが多いと思います。しかし、保育園で園児の位置を把握することを考えると、ネットワーク接続が必要になる受信部を壁や遊具に固定して小型の送信装置を園児の名前バッジや衣服などに装着する方が現実的です。

f:id:unifa_tech:20191208112151j:plain:w300:right それを考慮して今回は右のような構成で実験を行います。MacbookからiBeacon信号を送信し、Bluetoothが内蔵されているRaspberry Pi Zero WHを受信装置として3箇所(座標は既知)で信号強度を測定。3つの受信信号強度からそれぞれの距離(d1, d2, d3)を推定してそこからMacBookの位置を割り出します。なお、MacBookからのビーコン信号の送信にはNode.jsのblenoライブラリを使用しました。そして、Raspberry Piでの受信には姉妹ライブラリであるnobleとnobleをバックエンドとして動くnode-beacon-scannerを組み合わせて使いました。


信号強度から距離を推定する

iBeacon受信信号にはTxPowerとRSSI(Received Signal Strength Indication)というフィールドが含まれます。TxPowerはビーコンが発する信号の強さ(実際にはビーコンから1m離れた距離での受信信号強度、単位はdBm)であり、送信側で設定する値です。今回はMacBookから1m離れた地点での実測値(-54.5)を設定しています。一方RSSIは受信した信号の強度(dBm)です。この2つから送信地点と受信地点の距離dを次のように求めることができます(参考)。


d = 10^{\,(TxPower-RSSI)\,/\,(10*n)}

ここで、nはビーコン信号が遮蔽などなく理想的に伝達する空間の場合は2で、通常は環境に応じて最適化する必要があります。今回の実験ではn=2 としています。また、iBeaconはdefaultでは100msごとに信号をアドバタイズしますが、RSSIの値は比較的変動するため各地点とも数秒間の平均をとりました。その結果MacBookと各受信地点との距離(m)は次のように求まりました。


d1 = 1.084, \, d2 = 1.135, \, d3 = 3.055


距離から位置を推定する

各受信地点までの距離が正確に求まる場合は、各地点を中心として半径がそれぞれの距離である円を描くと3つの円が1点で交わります。その点が送信位置です。しかし実際には必ず誤差がのるためそのような幾何的な手法で求めることはできません。そこで、送信位置をxとして、各受信地点との距離の平均二乗誤差を最小にする最適化問題として解いていきます。

import numpy as np

def distance(pos1, pos2): #二点間の距離
    return np.sqrt(sum( (np.array(pos1)-np.array(pos2))**2 ))

def mse(x, locations, distances): #距離の平均二乗誤差
    sum_sqerr = 0.0
    for loc, dist in zip(locations, distances):
        dist_calculated = distance(x, loc)
        sum_sqerr += (dist_calculated - dist)**2
    return sum_sqerr / len(locations)

def midpoint(*args): #中点
    return np.mean(np.array(*args), axis=0)

上のmseが最小にしたい関数です。このように定式化することで受信箇所の数が3より大きい場合にも同様に扱うことができます。最適化問題は例えば下記のようにscipyに含まれるminimize関数を使って解くことができます。

from scipy.optimize import minimize

rec1 = [0, 0]      #受信1の座標
rec2 = [0.1, 1.9]  #受信2の座標
rec3 = [3.65, 2.1] #受信3の座標
d1 = 1.084
d2 = 1.135
d3 = 3.055

locations = [rec1, rec2, rec3]
distances = [d1, d2, d3]
initial_loc = midpoint(locations)

result = minimize(mse, initial_loc, (locations, distances))

f:id:unifa_tech:20191208112229j:plain:w300:right 上のコードで3つの受信地点の座標とそこまでの距離を入力として、距離の平均二乗誤差が最小になる送信地点を求めています。このとき最適化における初期値として3つの受信位置の中点を使っています。この結果求められた送信位置は(0.74, 0.91)でした。正解は(1.2, 1.0)ですのでぴったりではないですがおおよその場所は捉えられています。


まとめ

ビーコンの信号強度を使って送信位置を推定する方法を実装して悪くない結果を得ることができました。今回の実験では送信と受信の間には何も置かず、まわりにもBluetooth機器がそれほどなかったので比較的整った環境でしたが、それでも50cm弱の位置推定誤差になりました。実際の環境では壁、天井、人や物による反射や遮蔽、あるいは他のBluetooth信号との混線などもっとたくさんの誤差要因があります。そういった状況では上述のnの最適化だけでは不十分なことが多く、精度を保つために様々な手法が開発されています。実際のサービスに組み込んでいくためには求められる精度の理解とそれを実現するための必要十分な技術を見極めていくことが重要です。