ユニファ開発者ブログ

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

Ruby Bizグランプリ2019!

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

このあいだの休日、優雅に家で読書をしていたところ何やら水音が……。 嫌な予感がしてそっと洗面所を覗いてみたところ、我が家のドラム式洗濯機が前面ドア下方からダバダバと水を吐いているではありませんか! 慌てて洗濯機の運転を止めましたが、あたり一面びしゃびしゃ。 せっせと雑巾で拭き掃除をし、洗濯機の修理依頼をし、なんとも慌ただしい週末でした。 (ちなみにドアについてるパッキンが割れてました。。。) 思いがけず早めの洗面所大掃除ができたことだけが救いです。。。

そんなことが起こる数日前、私は帝国ホテルにてRuby Bizグランプリの表彰式に出席しておりました! 本日はその時の模様をお届けします。

Ruby Bizグランプリって?

公式サイトでの案内はこちらのようになっています。

ビジネスの領域においてプログラム言語 Ruby の特徴を活かして、新たなサービスを創造し世界へ発信している企業、団体及び個人を対象としたグランプリです。

rubybiz.jp

今年は午睡!

「今年は」と書いたのは、実は以前にPhotoでも応募させて頂いたことがあるためです。 その時も、ブログのネタとしてこちらで書かせて頂きました。

tech.unifa-e.com

前回は特別賞を頂き、大変嬉しかった記憶があります。 今回は午睡チェックサービスでグランプリが取れたらいいな!ということで、応募期限ギリギリで応募書類を提出。(事務局のみなさま、申し訳ありません。。。) 今年は33のサービスが応募したということでした。 後日、表彰式への招待=ノミネートされたことの連絡を頂き、「もしかして今年こそ!?」という期待を胸に会場へ。 しかし今回の私は、のほほんとしている訳にはいきませんでした。 なぜなら、私が登壇することになっていたからです……。。。

いざ会場へ。

会場の帝国ホテルは、実は現在のオフィスから徒歩数分の距離。 偶然にも暖かい日だったので、とても助かりました。

f:id:unifa_tech:20191216182009p:plain
会場入り口にある紹介用のボード
今回も普段はお会い出来ないような方々がいらしていて、それだけでどきどきです。 私は登壇者でしたので、登壇者用の控室がありまして、そこでコーヒーを頂きつつ時間まで待っていました。 控室では和やかにお話しされる方、プレゼンの練習をされている方、など色々いらっしゃいまして、嫌が応にも緊張感が高まります。

念願は叶うのか。

オープニングセレモニー、色々な方々のご挨拶に続き、表彰の時間がやってきました。 Ruby Bizグランプリは最終ノミネート10サービスのうち、大賞2本、特別賞3本、それ以外の賞が5本という構成です。 大賞が一番最後の発表なのですが、当日のこの発表の時間まで本当に、自分のところが何を受賞したのか知らされていないので、いつ呼ばれるかとどきどきしながら待ちます。

結果は……

f:id:unifa_tech:20191216184054p:plain
特別賞を頂きました!
慣れないパンプスでワタワタしながら壇上へ。
f:id:unifa_tech:20191216184456p:plain
緊張で顔がこわばっております。

受賞された他の会社さんがみなさま、とても素晴らしいコメントをされる中、私は「実は今年はグランプリが欲しいと思ってました。」と素直すぎるコメントをし(すみません。。。)、合わせてその場でまた別のプロダクトで今度こそはグランプリを取れるように頑張ろうと思います!と言うお話をさせて頂きました。

しかしながら、今回大賞を取られた2プロダクトのお話を受賞者プレゼンで拝聴し、その内容の素晴らしさ(Rubyを利用したことの意義や、Rubyの特徴をよりよく活用しているなど)に納得してオフィスに戻りました。

実はもっと話したいことがあった。

実は、コメントの内容はいくつか考えていたのですが、最終的に持ち時間を考えて削った内容がありました。 せっかくなので、その一部をこちらで書こうと思います。

このサービスの構想自体は2016年の年末頃には既に存在しており、前回のフォトでの受賞の折のプレゼンテーション資料の中にも今後の展望の一つとして記載をされていました。そこから企画や構想を一年ほど行い、開発を三、四ヶ月、プロトタイプを利用した数ヶ月のフィジビリティの中での微調整を重ね、ようやく正式なサービスを開始したのが2018年の4月という構想開始からの期間の比較的長いプロダクトでした。 そのような背景もあって、特にサービスの初期開発に携わったメンバーにとっては大変に思い入れも深く、同時にたくさんの新しい経験と苦労をしたプロダクトです。 企画、デザイン、テスト工程を含む開発のみならず、売り方やご利用開始の後のサポート、フォローの仕方など、どれひとつとってもそれまでのPhotoサービスとは異なり、試行錯誤を重ねながら今日までやって参りました。 2018年4月に開始し、現在では数千の園でご利用いただくまでに成長出来たことは大変に喜ばしく、また身が引き締まる想いでおります。

まとめ

  • 時々しかお外に出ませんが、こうして表彰して頂けると言うのはひとつの区切りとしてとても良いなと思いました。 (なかなか難しい午睡と言うプロダクトの企画、デザイン、ディレクション、開発、テストに携わってくださっているみなさま、本当にお疲れ様です。)

  • 今後もより一層、Rubyの発展に尽力すると共に、新しい技術を取り入れながらまたいつか表彰式に呼んでいただけるように頑張ろうと思います。

Adobe MAX Japan 2019

デザイナーの三好です。

先日パシフィコ横浜で行われたハイパーテンションウルトラクリエイティブイベント「Adobe MAX Japan 2019」に参加してきましたのでレポートを書きたいと思います。

Adobe MAX Japanについて

f:id:unifa_tech:20191206111907j:plain

https://maxjapan.adobe.com/

Adobe社主催で年に一度アメリカ、ヨーロッパ、日本で行われる世界最大級のクリエイティブイベントです。 Adobeソフト最新機能の発表や活用方法の解説、というのが名目となっているようですが、トップクリエイター達のセッションの他にもワークショップが行われていたり、グッズ販売、クレープやケバブの出店、Beer Bash、風船を片手に彷徨うおじさん達(スタッフ)、Adobeロゴがくまなく印刷された全身タイツの男がうろついていたりする狂乱のお祭りでした。

デザイナー経験の日が浅い私でさえ高鳴りましたので、Adobe信者と呼ばれるクリエイターの方々のその心中たるや、さぞクリスマスの朝を迎える寝不足の子どものような気持ちだったことでしょう。今年は過去最高の5000人以上が動員したようです。

クリエイティブの未来像

まず驚いたのがAdobe製品の膨大な数と進化のスピードです。特に私はまだひよっこで日常的に使えるソフトが限られているので、恥ずかしながら会場で初めて触れる製品がたくさんありました。

デジタルで完ぺきな水彩画表現を可能とするAdobe Fresco、直感的に扱える3DグラフィックデザインソフトAdobe Dimension、フォトグラファーとして活動する私としては強い関心のある最新アプリPhotoshop Camera。これらの超未来的ソフトウェア群がアドビAI(人工知能)Adobe Senseiの大活躍により、従来のPhotoshopのような専門的な知識と経験を必要としなくてもわりと気軽に扱えることができるというのが凄い。

2020年にリリースされるPhotoshop Cameraに関しては、スマートフォンアプリとしての提供ということから、プロ向きではなく一般的なSNSユーザーをターゲットにしてることがわかります。当然他のカメラアプリとは一線を画す超高度なアーティスティックな写真が誰でも撮れるようになるでしょう。

自身の立場で考えると「このままではいつかAdobe Senseiに仕事奪われちゃうのでは…」と不安がよぎりそうですがそこはクリエイターの味方Adobe。「Adobe Senseiは人間の仕事を奪うものではない。助けるものだ」と偉い人がおっしゃっているように、あくまでユーザーがこれまで以上にクリエイティブに専念できる環境を作る為の超優秀なアシスタントという役割だそうです。

アシスタントに負けないよう吐血しながら日々勉強あるのみですね。

f:id:unifa_tech:20191206113547j:plain

まとめ

まるで大きなクラブのような(会場内の暗さとライティングの派手さ然り)雰囲気で本家がロサンゼルスであることも納得のバブリーなイベントで、Adobeのスローガンである ”Creativity for all” の精神を至るところで触れることができとても良い刺激になりました。

そしてこれは技術職と呼ばれるもの人全てに共通することかもしれませんが、この仕事を続けていくには瞬きをする間に進化していく業界に常に笑って食い下がっていかなければならないということを再認識しました。

笑えるくらい愛せること、永遠に学ぶことを恐れないというのがクリエイティブに携わる絶対条件で、その覚悟を改めて感じることができたのが1番の収穫です。

f:id:unifa_tech:20191206113528j:plain

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というものを知ったので使ってみます。

続きを読む