Hatena::ブログ(Diary)

Over&Out その後 このページをアンテナに追加 RSSフィード Twitter

2014-12-29

フリーランスを1年やってみて考えが変わったこと

お客さん仕事はわりと早いうちに納まったのですが、原稿書きがまったく納まる気配がないので、(現実逃避として)今年1年をふりかえろうと思います。


といっても「こういうお仕事をやりました」的なのは3ヶ月ごとにまとめているので、

ここでは主に、独立して最初の1年目 *1 を振り返ってみて「フリーランスになってみると意外だったこと」「考えが変わったこと」「やりかたを変えたこと」あたりの話を書こうと思います。


出勤した方が楽しい

独立当初は、フリーランスという働き方のメリットのひとつが、「場所・時間の自由」だと思っていて、必要な打ち合わせのとき以外は、家とか、自分が入居しているコワーキングスペースで作業するようにしていました。


で、お客さんもほとんどの場合それでいいと言っていただけるのですが、今は逆に、僕の方から「オフィスで一緒に仕事させてほしい」とお願いしています。


その理由としては、

  • お客さんにすぐ確認できる・・・優先度や不明点を確認でき、作業の無駄が減る
  • 遠隔でのコミュニケーションが苦手・・・誤解を与えない言い回しに苦労したり、でもそのあたりをサボるとお互い不要にイラッとしたり
  • 温度感がわかる・・・直接話したり、一緒にランチしたりする中でその会社の状況とか雰囲気とか、やろうとしていることが実感値として理解できる
  • 集中できる・・・やはり1人でやるより緊張感がある
  • 見積もりレス・・・後述

などなどあります。


寂しくない

「会社には仲間がいるが、フリーランスは1人でやるので、寂しい」


独立前は僕もそう思っていたのですが、意外とやってみると逆の印象を受けました。

  • フリーランスでも結局開発体制としてはチームの一員であることに変わりはないので、「一緒につくっている仲間」という感は会社のときと同様にある
  • 個人的なコミュニケーションも仕事に繋がったりするので、勉強会や飲み会に行く時間が以前よりもっと有意義に感じるようになった

といった理由から、独立前よりもコミュニケーションや人付き合いは活発になった感があります。


個人でも楽しい案件に関われる

フリーランスになるにあたって迷いが少しあって、その理由のひとつが「会社の大看板に来る仕事の方が楽しいんじゃないか」というものでした。実際、カヤック時代にやった仕事はどれもおもしろくて、やりがいがあって、実績になるものだったので。


で、蓋を開けてみると、今年やらせていただいた仕事はどれもおもしろくて、やりがいがあって、実績にもなるものばかりでした。


今年は運がよすぎただけかもしれませんが、とにかく粛々と腕を磨いて、ちゃんとやりたいこととかやったことをアピールし続ければ個人でも道は開けるものだなと。


見積もりを(あまり)しなくなった

見積もりが結構苦手でして。。


たいていの場合、見積もり時点でUIデザインとか、動き(遷移とかタップしたときのギミック的なもの)とかって決まってることってほとんどなくて、でもiOSアプリの場合そこがそもそも開発の肝だったりするわけで。標準UIコンポーネントのプロパティをいじる範囲で済むなら3分だけど、自分でカスタムUIコンポーネントつくるなら丸1,2日かかるかもしれないとか。


で、ほとんどの場合、企画書にある情報だけだと


「1人日〜3人月ですね」*2


とか答えたくなるわけですが、お客さんに対してそういうわけにもいかないので、


「ここはどうしますか?たとえばこうしたい場合、選択肢としてはAとBがあって、それぞれメリット・デメリットは・・・」


とかやりたいことを引き出すことが必要になり、そういう不明確なところを質問にまとめたり、A案・B案みたいな枝分かれをさせつつ語弊や認識違いのないように・・・と回答を整理していくと、見積もりだけで8時間とか平気でかかってしまいます。


かといってざっくり見積もるとトラブルの元になるのでよくない、かといってお客さんからすると見積もりがないと始まらない、最初からUIデザインや詳細要件が明確になっていてほしいというのも無理がある。。



そんなわけで、最近は「お客さんのところに行って1日作業して、その日数 ✕ 人日ぶんいただく」という稼動日ベース方式でやっています。いつどれぐらいお客さんのところに行って作業するかは、その時々でお互いの状況や残りタスクの量を見て相談する感じで。


このやり方だとお客さん側にもいろいろとメリットがあると思っていて、

  • 状況にあわせて仕様とか優先順位を日々変更できる
  • 見積のバッファみたいなものが入ってこないので、無駄が少ない

みたいなことがあるかと。


ただこれって、「堤の1日の作業」に対するお客さんの信頼ありきなので、最初は1人日でどれぐらいできそうか、みたいな目安は提示したりはします。



ちなみにプロトタイプ系の案件は例外で、規模的に数人日レベルのものは十分見通しが立つので、お客さんに不明事項を確認して普通に見積もり出してやってます。*3


おわりに

先にもちょっと書いたように、もともとフリーランスになるにあたって迷いもあったわけですが *4、約1年やってみた今となっては、働き方としてかなり自分に向いてるんじゃないか、と感じています。

  • 自己責任で、自分の働き方・生き方を自由度高く選択できる
  • 自分の行動の結果が、自分にダイレクトに返ってくる

というあたりがやっててとても心地いいなと。


というわけで2015年も引き続きフリーランスとしてがんばっていく所存ですので、どうぞよろしくお願いいたします!ではみなさま良いお年を。


*1:ちなみに2014年2月に独立しました。

*2:標準コンポーネントを並べるだけで、「それっぽく」つくるだけなら結構1日でできたりしますよね

*3:あと、今はスタートアップのお仕事が多いのでたまたまこれで成立しているだけで、もうちょっと規模の大きい会社とお仕事させていただく場合は普通に見積もりレスなんて無理かもしれません。

*4:独立の経緯について話したインタビュー: http://prosheet.jp/blog/engineer/4641/

2014-12-19

iOSエンジニアが初めてAndroid開発をやってみた第1日目のメモ

当方フリーランスエンジニアですが、iOS専業でやっております。これまで幾度と無く「ちなみにAndroidの方は・・・?」「すいません、できないんです・・・」と肩身の狭い思いをしてきましたが、ついに今日第一歩目を踏み出しました。


ちなみに現在のスペックですが、

  • iOSアプリ開発歴4年、Android開発歴ゼロ
  • Androidはユーザーとしても経験値ほぼゼロ(先週 Nexus5 購入して WiFi セットアップしただけ)

という感じです。


実況中継的に随時追記する形で書いていきたいと思います。iOS からプログラミングの世界に入り、これから Android もやってみたい、という人も今となっては結構多いんじゃないかと思うので、どなたかの第一歩目の参考になれば幸いです。


※(終了後に追記)あとから読んだ人には何のことかわからないかもしれませんが、本記事は昼ぐらいに出だし(午前中に進めた内容。書籍購入まで)をアップし、その日何かやる度に実況中継的に追記していったものです。思いつくままに手を付けていった順に書いてるので、勉強しやすい順序とかにもなっていないと思います。


Android Studio インストール

何はともあれ開発環境のインストールから。(Android Studioは、iOSでいうXcode みたいなもの)

  • http://developer.android.com/sdk/index.html
  • Setup Wizard 通り
  • 途中で JDK 7.0 のダウンロード&インストールが必要(リンクはウィザード内で提示されるので事前準備は不要)
  • Standard と Custom 選択するところがあるがとりあえず Standard
  • License Agreement の後、そこそこ長いダウンロードとインストールがある

プロジェクト作成&エミュレータで動かしてみる

開発環境を入れたところで「で、どうしようかな。。」と一瞬途方に暮れたかけたのですが、iOSだとどうするかな、と想像してみて、何はともあれプロジェクト作成してエミュレータで空のプロジェクトを動作させてみるのがいいのかなと。

  • バージョンはとりあえず 4.4 (自分の Android がそうだったので)以上
  • テンプレートは違いがわからないので Blank
  • ActivityNameとかLayoutNameなどなど、それぞれがどこに影響するかわからないのでデフォルトのままで

で、上部のバーに Xcode の Build & Run ボタンと同じようなのがあったので、


f:id:shu223:20141219131748p:image


押してみると、デバイス選択っぽいのが出てきて、無事エミュレータで動作しました!


f:id:shu223:20141219131817p:image:w250


ただ、ロック画面みたいなところから起動したので、Androidの操作自体がわからなくてちょっと困惑しましたが。。


書籍を入手

最初はWebでググるにも用語自体がわからなかったりするので、近所の書店に行って、参考書籍を購入しました。


つくるべきものは決まっているので、サンプルをチュートリアル的に進めるタイプのものではなく、「テーブルビュー的なのはどう実装するんだろう」という感じで逆引き的に調べられそうかどうか、を判断基準に、書店で目次を確認して選びました。


書籍1:Android StudioではじめるAndroidプログラミング入門

Android StudioではじめるAndroidプログラミング入門
掌田 津耶乃
秀和システム
売り上げランキング: 37,320


入門とありますが目次見るとわりと網羅的な感じがしたので購入しました。今こうやって見るとAmazonではなかなか厳しいレビューついてますね。。


書籍2:Androidアプリ開発逆引きレシピ (PROGRAMMER’S RECiPE)

Androidアプリ開発逆引きレシピ (PROGRAMMER’S RECiPE)
株式会社Re:Kayo-System
翔泳社
売り上げランキング: 94,035


逆引きレシピが欲しかったので最初に手に取りました。出版年月も新しめだし。


書籍3:Androidアプリ開発パーフェクトマスター―Android4/3/2.2完全対応 (Perfect Master)

Androidアプリ開発パーフェクトマスター―Android4/3/2.2完全対応 (Perfect Master)
金城 俊哉
秀和システム
売り上げランキング: 41,222


分厚すぎて絶対外に持ち出せないし、実用的じゃないんじゃないか。。と思いましたが、情報量は多いし、悩んでる時間がもったいないので購入。


UIImageView的なものを置いてみる

画像リソースの実態は、(フォルダ名の一般名称がわからないので自分の現在のプロジェクトで言うと)

MyFirstApplication/app/src/main/res

にあります。


が、画像の追加はプロジェクトウィンドウ内にある「drawable-mdpi」フォルダにドラッグ&ドロップするとのこと。(書籍1からの情報)


UIImageView的なUIコンポーネントは、Widgetsのところに「ImageView」というそのものズバリなものがあったので、それを配置。


画像の入れ方がよくわからなかったのですが(書籍1ではプログラムから入れていた)、ImageView を IB 的なところでダブルクリックしたら src 画像を選ぶダイアログが出てきて、無事 drawable-mdpi に入れたものを選択することができました。


f:id:shu223:20141219142442p:image:w220


UIViewContentMode的なプロパティと、このレイアウト周りがわからないけど、そのあたりは後回しで。。


ボタンを置いてみる

ボタンを押したらコンソールにログをはく、的なことをやってみます。

  • Widgetsに「Button」というのがあるので、それを配置
  • 右にあるプロパティウィンドウから、「onClick」を探して、「button_onClick」と入力
  • MainActivity.javaを開き、ボタンクリックのイベントハンドラを実装
public void button_onClick(View view) {
    Log.d("MyApp", "view:" + view);
}

(記念すべき初めてのコード!)


ボタンまわりは書籍1を、Log の書き方はググって下記記事を参考にしました。


で、これだとエラーが出たので、import を追記

import android.view.View;
import android.util.Log;

(使用クラスごとに必要なのか・・・!?そんなわけないと思うけど、おいおい学んでいきます。。)


f:id:shu223:20141219142623p:image:w220


ボタンを押すと、コンソールに次のように出力されました。

12-19 14:21:48.467 1911-1911/com.shu223.myfirstapplication D/MyApp﹕ view:android.widget.Button{9cf6a48 VFED..C. ...P.... 372,921-707,1065 #7f080041 app:id/button}


画面遷移

次は UINavigationController の push / pop 的な画面遷移を・・・と思ったのですが、買った書籍(総ページ数約2000!)を調べてみても見当たらず・・・*1


ググるとすぐに出てきました。


インテントは他のアプリと連携するときに用いるもので、他のアプリ(例えばブラウザアプリ、メールアプリ等)を起動することができます。


同じアプリでも他の画面 (Activity) クラスを起動する場合には、インテントを使用します。

そうなんですね、カルチャーショック。。


それはさておき、

画面の遷移は, 元画面(メイン画面)のアクティビティから次画面(サブ画面)のアクティビティを起動することで実現できる.

とのことなので、

  • メニューから新規アクティビティを作成
    • [File] > [New] > [Activity]
    • SubActivityと命名
  • SubActivity に Button を置き、onClick プロパティに button_onClick と入力
  • SubActivity.java に以下のコード(と必要なimport)を追記
public void button_onClick(View view) {

    Log.d("MyApp", "onClick:" + view);

    Intent intent = new Intent(SubActivity.this, MainActivity.class);
    startActivity(intent);
}
  • MainActivity.java のボタンのイベントハンドラを以下のように修正
public void button_onClick(View view) {

    Log.d("MyApp", "onClick:" + view);

    Intent intent = new Intent(MainActivity.this, SubActivity.class);
    startActivity(intent);
}

上記記事では AndroidManifest.xml へ SubActivity の定義を追加する必要がある、とありますが、自動的に追加されてました。


f:id:shu223:20141219152052g:image


テーブルビュー的なものを実装する

iOS でいう UITableView は、Android では「リストビュー」と呼ぶそうです。で、「アダプター」なるものが UITableViewDataSource 的役割をするっぽい(いや、それ以前の、単なる配列かも。。)

  • Containers にある「ListView」を配置
  • SubActivity.java の onCreate メソッド内に、以下のコードを追記
ListView listView = (ListView)findViewById(R.id.listView);
ListAdapter adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, DAYS);
listView.setAdapter(adapter);
  • 同じく SubActivity.java に、以下のコードを追記(static な配列を生成)
private static final String[] DAYS = new String[] {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

以上、「書籍2」の写経です。正直なところ、`R.id.listView` の `R` とか、`android.R.layout.simple_dropdown_item_1line` が何なのかとか全然わかっておりません。。


f:id:shu223:20141219162621p:image:w220


ちなみに コードから IBOutlet 的に UI オブジェクトにアクセスするにはどうしたらいいのか と思ってましたが、上記のように `findViewById` メソッドで取得するのが定石のようです。iOSでいえば `getViewWithTag` ですね。id は自動補完で出てきますが、IB的な画面の Properties の「id」からも確認できます。


実機で動かしてみる

ここまで忘れてたのですが、せっかく Android 端末を買ったので、実機で動かしてみます。


実機の「デベロッパーモード」を有効にする必要があるとのこと。「書籍3」に全編スクショ付きで載ってたので、Androidの使い方がわからない自分でも簡単でした。

  • [設定] > [端末情報]
  • 「ビルド番号」のところを7回タップ(おもろいw) -> デベロッパーモードになる
  • [設定] > [開発者向けオプション] > [USBデバッグ] を有効にする

あとはエミュレータの代わりに、実機を選択するだけ。


f:id:shu223:20141219165446j:image:w300


CSVファイルの読み込み

書籍に見当たらなかったのでググりました。


新規クラスファイルの生成方法がわからなくてけっこう困ったのですが、

app/src/main/java の位置で [File] > [New] しないと、メニューに「Java Class」というのが出ないようです。さらに、「com.shu223.myfirstapplication」と書かれているディレクトリ(パッケージ?)の下に置くと、他のクラスから扱えるようになるようです。


あと「プロジェクトの一番上」という表現もよくわからなかったのですが、 app/src/main 直下に assets という名前でディレクトリを作成すればOKでした。


  • assets ディレクトリを作成
    • 直下に csv ファイルを置く
  • CSVParser クラスを作成
package com.shu223.myfirstapplication;

import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

/**
 * Created by shuichi on 14/12/19.
 */
public class CSVParser {

    public static void parse(Context context) {
        // AssetManagerの呼び出し
        AssetManager assetManager = context.getResources().getAssets();
        try {
            // CSVファイルの読み込み
            InputStream is = assetManager.open("params.csv");
            InputStreamReader inputStreamReader = new InputStreamReader(is);
            BufferedReader bufferReader = new BufferedReader(inputStreamReader);
            String line = "";
            while ((line = bufferReader.readLine()) != null) {
                // 各行が","で区切られていて4つの項目があるとする
                StringTokenizer st = new StringTokenizer(line, ",");
                String first = st.nextToken();
                String second = st.nextToken();
                String third = st.nextToken();
                String fourth = st.nextToken();

                Log.d("MyApp", "Parsed:" + first + second + third + fourth);
            }
            bufferReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  • SubActivity から呼んでみる
CSVParser parser = new CSVParser();
Context context = getApplicationContext();
parser.parse(context);

Context についてはググって下記記事を参考にしました。


実行結果については省略。


ボタンに応じてクリックイベントの処理を分岐させる

MainActivity にボタンを2つ置いて、iOS でいう tag で判別して次に遷移する画面を切り替える、みたいなことをやるには、id プロパティを使用するようです。

  • MainActivity.xml にボタンをもう1つ追加
  • 2つのボタンの id をそれぞれ、「button1」「button2」とする
  • MainActivity.java のイベントハンドラの実装を次のように変更する
public void button_onClick(View view) {

    switch (view.getId()) {
        case R.id.button1:
        {
            Intent intent = new Intent(MainActivity.this, SubActivity.class);
            startActivity(intent);
            break;
        }
        case R.id.button2: {
            Intent intent = new Intent(MainActivity.this, BLEActivity.class);
            startActivity(intent);
            break;
        }
    }
}

参考:クリックイベントに応答する - Android 開発入門


プロジェクトの API LEVEL を変更する

「APIレベル」って初耳でそもそも何なのかわかってないのですが、BLE 関連のコードを書き始めると、巷に情報がよく出ているサンプルコードの多くは `startLeScan`、 `stopLeScan` という BluetoothAdapter クラスのメソッドを使っていて、どうやらこれらは API LEVEL 21 で deprecated になっているらしい。


で、仕方なく推奨されている新しい方を使おうと思ってみたものの、付け焼き刃すぎて、サンプルがないとよくわからない。。


APIレベルってそもそも何なの?と調べてみると、

21とか全然新しすぎる模様。iOS でいえば 8 の API を使ってたようなものか。


で、どこで API レベルが設定されていて、どう変更するのが適切なのかなと調べてみると、build.gradle というファイルを更新する、と書いてあったのですが、

Android Studio の [File] > [Project Structure] > [Flavors] からも変更できました。


ここで「Target Sdk Version」を 19 に変えて、「Sync Project with Gradle Files」ボタン押下、リビルドしました。


結果・・・deprecated なメソッドの警告はなくなりませんでした。。


で、もう一度 [Project Structure] から、[Properties] の 「Comple Sdk Version」 にマニュアルで 19 といれてみたところ、SDKのインストールが始まって、なんやかんやあって、無事「API LEVEL 21 では deprecated だけど、それより下のバージョンでは OKなメソッド」の警告が消えました。。


BLEを利用する

BLE(Bluetooth Low Energy)については参考書籍3冊とも一切記載なし。


が、ググるといろいろと参考記事が見つかりました。


実際にやってみると、deprecated 問題(前項参照)とかいろいろ大変だったのですが、下記手順でいけました。

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
  • メンバ変数などの追加
private static final long SCAN_PERIOD = 10000;
private BluetoothAdapter mBluetoothAdapter;
private Handler mHandler;
static final int REQUEST_ENABLE_BT = 0;
  • 諸々初期化処理(onCreateにて)
BluetoothManager manager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();

if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

mHandler = new Handler();
  • スキャン開始ボタンのイベントハンドラ
public void onClickScan(View v) {

    Log.d("MyApp", "onClickScan");

    // 10秒後にスキャン停止
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    }, SCAN_PERIOD);

    // スキャン開始
    mBluetoothAdapter.startLeScan(mLeScanCallback);
}
  • ペリフェラルが見つかると呼ばれるコールバック
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        // 見つかったペリフェラル情報をログ出力
    }
};

実行してみると、無事部屋にあるペリフェラルデバイスが見つかってました。

12-19 19:38:38.578    2287-2299/com.shu223.myfirstapplication D/BLEActivity&#65109; name=Kontakt, bondStatus=10, address=EE:FB:3B:6C:B8:BE, type2, uuids=
12-19 19:38:38.768    2287-2300/com.shu223.myfirstapplication D/BLEActivity&#65109; name=null, bondStatus=10, address=59:F9:0E:E7:AD:41, type0, uuids=
12-19 19:38:38.958    2287-2299/com.shu223.myfirstapplication D/BLEActivity&#65109; name=null, bondStatus=10, address=59:F9:0E:E7:AD:41, type0, uuids=
12-19 19:38:39.078    2287-2300/com.shu223.myfirstapplication D/BLEActivity&#65109; name=Kontakt, bondStatus=10, address=EE:FB:3B:6C:B8:BE, type2, uuids=
12-19 19:38:39.128    2287-2299/com.shu223.myfirstapplication D/BLEActivity&#65109; name=null, bondStatus=10, address=59:F9:0E:E7:AD:41, type0, uuids=
12-19 19:38:39.318    2287-2300/com.shu223.myfirstapplication D/BLEActivity&#65109; name=null, bondStatus=10, address=59:F9:0E:E7:AD:41, type0, uuids=
12-19 19:38:39.498    2287-2299/com.shu223.myfirstapplication D/BLEActivity&#65109; name=null, bondStatus=10, address=59:F9:0E:E7:AD:41, type0, uuids=

(iOS のセントラルマネージャのように、重複するペリフェラルについてはコールバックをまとめる、ということはやってくれない


アニメーション

PulsingHalo的なのを実装したくて着手していたのですが、所用につき今日はここまで。*2


所感

Android Studio という専用 IDE があるし、エミュレータもまぁ確かに遅いけど全然アリだし、関連書籍は充実してるし、ググれば大量に開発情報が出てくるしで、開発環境としては超快適だと思いました。iOSと比べてどうの、という不満は全くなし


ただ、やはり Android は、API群(SDK)がまるっきり違う上に開発言語や IDE も違うので、思った以上に「覚え直し」感がありました。。ひとつのアプリをプロダクト品質で出すまでにはまだまだ先が遠い印象。。


最近始めて Mac OS X アプリをつくったときは新しい世界が切り開かれる感じがしてワクワクしたのですが、やはり「モバイル端末でのアプリ開発」という似ている分野では自分はあまり新鮮味を感じられないのかなと。(ObjC->Swiftへのコンバートにもあまり興味が無いように)


そんなわけで、1日やってみた後の所感としては「おとなしく iOS の世界に戻ろう」という気持ちになっております。。


*1:あとでわかってみれば、載っていました。が、そもそもインテント、アクティビティという概念を知ってないと目次からは辿りつけない。。

*2:この後の予定としては、「レイアウト」「ボタン等の見た目のカスタマイズ」「デバッグ」などなど予定していました。

2014-12-12

iOSと機械学習

ビッグデータとかの機械学習隆盛の背景にある文脈や、その拠り所となるコンピュータの処理性能から考えても「モバイルデバイス向けOSと機械学習を紐付けて考えようとする」ことはそもそもあまり筋がよろしくない・・・とは思うのですが、やはり長くiOSだけにコミットしてきた身としては、新たに興味を持っている機械学習という分野と、勝手知ったるiOSという分野の交差点はないのかなと考えずにはいられないわけでして。。


そんなわけで、「iOS と機械学習」について雑多な切り口から調べてみました。


iOSで使える機械学習ライブラリ

DeepBeliefSDK

コンボリューショナルニューラルネットワークを用いた画像認識ライブラリ。iOSとかのモバイルデバイスで処理できるよう、高度に最適化してある、OpenCVと一緒に使うのも簡単、とのこと。


何はともあれ SimpleExample というサンプル実行したら、


f:id:shu223:20141211210821j:image


頼んでもないのにいきなりノートパソコンを認識しました!すごい!


SimpleExample のソースを見てみると、フレーム毎に得られるピクセルバッファの処理はこんな感じでした。

- (void)runCNNOnFrame: (CVPixelBufferRef) pixelBuffer
{
  assert(pixelBuffer != NULL);

	OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType( pixelBuffer );
  int doReverseChannels;
	if ( kCVPixelFormatType_32ARGB == sourcePixelFormat ) {
    doReverseChannels = 1;
	} else if ( kCVPixelFormatType_32BGRA == sourcePixelFormat ) {
    doReverseChannels = 0;
	} else {
    assert(false); // Unknown source format
  }

	const int sourceRowBytes = (int)CVPixelBufferGetBytesPerRow( pixelBuffer );
	const int width = (int)CVPixelBufferGetWidth( pixelBuffer );
	const int fullHeight = (int)CVPixelBufferGetHeight( pixelBuffer );
	CVPixelBufferLockBaseAddress( pixelBuffer, 0 );
	unsigned char* sourceBaseAddr = CVPixelBufferGetBaseAddress( pixelBuffer );
  int height;
  unsigned char* sourceStartAddr;
  if (fullHeight <= width) {
    height = fullHeight;
    sourceStartAddr = sourceBaseAddr;
  } else {
    height = width;
    const int marginY = ((fullHeight - width) / 2);
    sourceStartAddr = (sourceBaseAddr + (marginY * sourceRowBytes));
  }
  void* cnnInput = jpcnn_create_image_buffer_from_uint8_data(sourceStartAddr, width, height, 4, sourceRowBytes, doReverseChannels, 1);
  float* predictions;
  int predictionsLength;
  char** predictionsLabels;
  int predictionsLabelsLength;

  struct timeval start;
  gettimeofday(&start, NULL);
  jpcnn_classify_image(network, cnnInput, JPCNN_RANDOM_SAMPLE, 0, &predictions, &predictionsLength, &predictionsLabels, &predictionsLabelsLength);
  struct timeval end;
  gettimeofday(&end, NULL);
  const long seconds  = end.tv_sec-- start.tv_sec;
  const long useconds = end.tv_usec - start.tv_usec;
  const float duration = ((seconds) * 1000 + useconds/1000.0) + 0.5;
  NSLog(@"Took %f ms", duration);

  jpcnn_destroy_image_buffer(cnnInput);

  NSMutableDictionary* newValues = [NSMutableDictionary dictionary];
  for (int index = 0; index < predictionsLength; index += 1) {
    const float predictionValue = predictions[index];
    if (predictionValue > 0.05f) {
      char* label = predictionsLabels[index % predictionsLabelsLength];
      NSString* labelObject = [NSString stringWithCString: label];
      NSNumber* valueObject = [NSNumber numberWithFloat: predictionValue];
      [newValues setObject: valueObject forKey: labelObject];
    }
  }
  dispatch_async(dispatch_get_main_queue(), ^(void) {
    [self setPredictionValues: newValues];
  });
}

・・・と決してシンプルとは言い難いですが、ここで行っている分類処理のコアは、

jpcnn_classify_image(network, cnnInput, JPCNN_RANDOM_SAMPLE, 0, &predictions, &predictionsLength, &predictionsLabels, &predictionsLabelsLength);

この1行にあるのかなと。で、あとはバッファまわりの諸々とか、得られた結果の処理とか。


おもしろそうなので、また別の機会にちゃんと見てみようと思います。


ML4iOS

iOSでの機械学習を行うためのオープンソースライブラリ。ライセンスは Apache License, Version 2.0 。


コミット履歴を見ると、3年前(2012年1月)からあり、つい最近も更新されています。


LearnKit

iOS, OS X 向け機械学習フレームワーク、とのこと。


サポートしているアルゴリズム

  • Anomaly Detection
  • Collaborative Filtering
  • Decision Trees
  • k-Means
  • k-Nearest Neighbors
  • Linear Regression
  • Logistic Regression
  • Naive Bayes
  • Neural Networks
  • Principal Component Analysis

READMEに下記のようなサンプルコード載ってるので、今度試す。

LNKNeuralNetClassifier *classifier = [[LNKNeuralNetClassifier alloc] initWithMatrix:matrix 
                                                                 implementationType:LNKImplementationTypeAccelerate
                                                              optimizationAlgorithm:algorithm
                                                                            classes:[LNKClasses withRange:NSMakeRange(1, 10)]];
[classifier train];

LNKClass *someDigit = [classifier predictValueForFeatureVector:someImage length:someImageLength];

使いやすそう。


mlpack-ios

mlpack を Objective-C プロジェクトにリンクできるようにしたもの、とのこと。


mlpack というのは「スケーラブルなC++機械学習ライブラリ」らしい。


Swift-Brain

Swiftで書かれた人工知能/機械学習ライブラリ。ベイズ理論、ニューラルネットワーク、その他AIが実装されているとのこと。


  • Matrices
    • Matrix operations
  • Machine Learning algorithms
    • Basic Regressions
    • Neural Networks
    • Support Vector Machines
    • Bayesian Classifiers
    • Self Organized Maps (maybe?)
    • Clustering
  • Statistics
    • Bayes Theorem/Naive Classifier
    • Kalman Filter
    • Markov Model

学習結果をiOSで利用

機械学習によって得られたモデル等をiOSで利用する」ケースです。


冒頭で、「iOSと機械学習を結びつけて考えるのはあまり筋がよろしくない」と書きましたが、このケースでは学習自体はiOSデバイスではなくバックエンド(という表現が正しいかは不明)で行うので、至極真っ当、というか王道かと。


画像認識

OpenCV for iOS に機械学習で作成したモデル(分類器)のxmlファイルをアプリに持たせれば、人の顔以外にもいろんなものを認識できますよ、という記事。



分類器自体はプラットフォーム依存ではないので、iOSという限定を外せばかなり色々とチュートリアル記事とかモデル配布記事がでてきます。


文字認識

iOS で使える OCR ライブラリ。


機械学習で精度を向上させたり、日本語を覚えさせたり。


参考:画像からのテキスト抽出:tesseract-ocr - Qiita


音声認識

こういう話でいえば、同様に、iOS用の音声認識エンジンも学習データを用意してモデルを自作することができます。

OpenEars は PocketSphinx (CMU Sphinx) というカーネギーメロン大学によるオープンソース認識エンジンのラッパーライブラリで、それ用の音響モデルを自作することはできます(とある案件で試したことあり)。


国産の「大語彙連続音声認識エンジン Julius」(iOSで利用可能)も、もちろん学習によりモデルを自作できます。


実例集

ストアに出ているアプリなど。


Deep Belief by Jetpac - teach your phone to recognize any object

上でも紹介した、DeepBeliefSDKを使ったアプリ。


f:id:shu223:20141211211050j:image:w200


このアプリでネコを認識するデモ動画。


Deep Learning

ディープラーニングで画像を分類するアプリ。


f:id:shu223:20141211211118j:image:w200


Summly

2011年12月と、ちょっと古い記事ですが、Summlyという「ウェブのコンテンツを箇条書きとキーワードの一覧に要約する」アプリのニュース記事。

まずは、特別なアルゴリズムでHTML処理を使って、ウェブページからテキストを抜き出すことから始まる。そのテキストを分析して、記事から選び出された「凝縮された部分」を箇条書きで吐き出す。Summlyのアルゴリズムは、いくつもの機械学習の手法と「遺伝的」アルゴリズム――進化をまねた発見的探索法――を利用してこれを行っている。

ダロイジオ氏のアルゴリズムは、さまざまな出版社によるいろいろなタイプの記事の、人間による要約を調査した。そしてこれらの要約を、Summlyが吐き出すべきものや、情報キュレーションを行う人間の仕事をうまく真似るための、メトリクス[尺度]を調整する際のモデルとして活用した。


その後 Yahoo! に買収され、Yahoo!のニュース要約とパーソナライズ表示、動画、画像検索機能等にその技術が用いられているとのこと。


Plant Recognition: Bringing Deep Learning to iOS

ディープラーニングで植物を認識する、という論文。


f:id:shu223:20141211211217j:image


おわりに

iOS と機械学習を絡めたお仕事、お待ちしております!


2014-12-11

「FILTERS」で学ぶ GLSL

GLSL を書いてオレオレフィルターをつくれる」というコンセプトのカメラアプリがリリースされました。


f:id:shu223:20141211164529j:image:w600

Filters〜世界一面白いカメラフィルターが、ここから生まれる。〜



シェーダを書いて動的に適用する、というアイデア自体は昔からあるものですが、


wonderfl や jsdo.it をつくったカヤック製アプリなので、

  • フォークできる
  • リアルタイムプレビューしてくれるエディタでコード(GLSL)を書ける
  • シェアできる

という非常に魅力的な点があります。あと前述の従来品はサンプルだったりするので、もちろんカメラアプリとしてのクオリティも全然違います。



既に魅力的なフィルタがいろいろとアップされています。


f:id:shu223:20141211164641g:image


GLSL には興味があったものの、なかなか勉強する機会がなかったので、他の方がつくったフィルタをフォークしてコード(GLSL)を読みつつコメントを入れていくということをやってみました。


タイトルの後ろに (commented) と入れているので、よろしければ参考にしてみてください。*1


以下、いくつか紹介していきます。


グレースケールにする

入力画像をグレースケールに変換するフィルタ。


f:id:shu223:20141211164705j:image


これはフィルタを新規作成すると生成されるデフォルトテンプレートの最後の行だけいじってコメントを入れたもの。GLSLを書いてみる最初の1歩として、この3行を理解するといいかもしれません。


grayscale (commented)
void main()
{
    // 座標を取得
    vec2 uv  = iScreen;
    
    // カメラからの入力テクスチャから、該当座標の色を取得する
    vec4 color = texture2D(iCamera, uv);
  
    // RGBのGとBをRで置き換えた色を生成して適用
    gl_FragColor = vec4(color.r,color.r,color.r, 1.0);
}

歪める&青っぽくする

入力画像を歪め、全体的に青っぽくするフィルタ。


f:id:shu223:20141211164756j:image


iCamera(カメラからの入力テクスチャ) + texture2D関数で、入力画像の任意の座標の色をとってこれることを利用して、座標をx,yそれぞれ2乗した先の画素値をとってきて歪みを実現しています。


water (commented)
void main()
{
    // 座標取得
    vec2 scr = iScreen;
  
    // 座標に応じて近隣の色を取得する
    // (カメラの入力テクスチャから、注目座標のx,yを2乗した座標の色をとってくる
    vec4 color  = texture2D(
      iCamera,
      vec2(
        pow(scr.x, 2.0),
        pow(scr.y, 2.0)
      )
    );
  
    // 青っぽくして適用
    gl_FragColor = vec4(
      vec3(
        color.r *   0.0 / 255.0,
        color.g * 153.0 / 255.0,
        color.b * 204.0 / 255.0
      ),
      1.0
    );
}

画像の一部にモザイクをかける

ピンポイントにモザイクをかけるフィルタ。ピンチイン・アウトにも対応。


f:id:shu223:20141211164814j:image


モザイク処理は、座標値を floor 関数で粗くして周辺画素と同じ色を使用することで実現されています。


また、出力位置が円の中にあるかどうかでモザイクをかける・かけないを判定して、円形のモザイク処理が実現されています。


ピンチイン・アウトに対応しているので座標処理がちょっと複雑な感じがする場合は、いったん iSize をなくして考えてみるとわかりやすいかもしれません。


Pinpoint Mosaic
// 白色を定数として定義
const vec3 white = vec3(1.0, 1.0, 1.0);

// 座標positionが、半径size・中心座標offsetにある円の内側にあるかを判定する
bool inCircle(vec2 position, vec2 offset, float size) {
    float len = length(position - offset);
    if (len < size) {
        return true;
    }
    return false;
}

void main( void ) {
    // 座標取得
    vec2 uv = iScreen;
    // 座標を粗くする(15 x ピンチサイズ倍して切り捨て)
    uv = floor(uv * iSize * 15.0) / 15.0 / iSize;
    // 粗くした座標の色を取得
    vec4 color = texture2D(iCamera, uv);
    
    // 白色(whiteは定数として定義済み)を生成
    vec3 destColor = white;
    // 出力位置を計算
    vec2 position = (gl_FragCoord.xy * 2.0 - iResolution) / min(iResolution.x, iResolution.y);
    
    // 出力位置が円の内側にあるか?
    if (inCircle(position, iPosition, 1.0 + (1.0 - iSize))) {
        // モザイク化する(粗くした座標の色を使用)
        destColor = color.rgb;
    }
    else {
      
        // モザイク化しない(元々の色を使用)
        destColor = texture2D(iCamera, iScreen).rgb;
    }
    
    // 決定した色を出力出力
    gl_FragColor = vec4(destColor, 1.0);
}

所感

FILTERS、GLSLの勉強におすすめです!


(GLSL関連記事)


(余談)iOS 8 とシェーディング言語

シェーダが書けると iOS の Core Image のフィルタ(CIFilter)も自作できるようになります。(iOS 8 から)


拙作『iOS8-Sampler』にもシェーダを書いて作成したカスタムフィルタのサンプルがいくつか入っています。


(2014.12.12追記)他のアプリからFILTERSのフィルタを使う



ってツイートしたら、中の人からリプライがあり、なんと既に Photo Editing Extension 対応してるとのこと。すごい!


標準の「写真」アプリから試してみました。


f:id:shu223:20141212134814j:image


現状では、上位30個ぐらいのフィルタが選べるようです。



*1:なお、コードをシンプルにするため、処理に使われていない関数や定数は削除しました。

2014-12-10

アップルによるBluetoothアクセサリの設計ガイドラインに書かれていたこと

『Bluetooth Accessory Design Guideline for Apple Products』という、Appleによる公式ドキュメントがあります。Mac や iOS デバイス、iPod 等の Apple 製品の Bluetooth アクセサリの設計についてのガイドラインです。


本記事では、このドキュメントから iOS エンジニアも知っておいた方が良さそうな部分 を抜粋していきたいと思います。


※本記事は、Bluetooth Low Energy Advent Calendar 2014 - Qiita 9日目の記事です。


アドバタイズ間隔

アドバタイズ開始から少なくとも30秒間は、推奨アドバタイズ間隔である 20 ms を使用すべき」と明記されています。

To be discovered by the Apple product, the accessory should first use the recommended advertising interval

of 20 ms for at least 30 seconds.


で、この最初の30秒間で発見されなければ、バッテリー節約のため下記のより長いアドバタイズ間隔のいずれかを使用することを推奨する、と。

If it is not discovered within the initial 30 seconds, Apple recommends using

one of the following longer intervals to increase chances of discovery by the Apple product:

  • 152.5 ms
  • 211.25 ms
  • 318.75 ms
  • 417.5 ms
  • 546.25 ms
  • 760 ms
  • 852.5 ms
  • 1022.5 ms
  • 1285 ms

ちなみに BLE の規格で定められているアドバタイズ間隔は、最小 20[ms] 〜 最大 10.24[s]の範囲で 0.625[ms]の整数倍 なので、Appleによる「最初の30秒間の推奨アドバタイズ間隔」はBLE規格上の最小間隔でもあります。


iOSエンジニアとしては、このあたりのアドバタイズ仕様を把握しておくと、セントラルとしてふるまうアプリにおいてスキャンをどう行うかの設計や、iBeacon の利用する際に役立つかもしれません。


Connection Parameters

以下の全てのルールに従っていないと、接続パラメータ要求は(iOSから)拒絶される可能性がある、と書かれています。

  • Interval Max * (Slave Latency + 1) ≤ 2 seconds
  • Interval Min ≥ 20 ms
  • Interval Min + 20 ms ≤ Interval Max Slave Latency ≤ 4
  • connSupervisionTimeout ≤ 6 seconds
  • Interval Max * (Slave Latency + 1) * 3 < connSupervisionTimeout

If Bluetooth Low Energy HID is one of the connected services of an accessory, connection interval down to

11.25 ms may be accepted by the Apple product.


The Apple product will not read or use the parameters in the Peripheral Preferred Connection Parameters

characteristic


iOSエンジニアとしては、Connection Interval が「20 ms 以上」と明記されている部分は情報として非常に有用なのではないでしょうか。


こういう計算が考え方として正しいものなのか、ちょっと自信がない(他に考慮すべきことがあるかも)のですが、Connection Intervalが20msということは、1秒間に50回データの送受信が可能、ということなので、1パケット20byteとして、


20 * 50 = 1000 bytes / sec = 1k byte / sec = 8k bits / sec

というわけで、iOSとBLE接続するデバイスとの最大通信速度は約8kbps、ということがいえるのではないでしょうか。


他にも、connSlaveLatency(スレイブが送信するデータがなにもないときに、コネクション・イベントを無視出来る回数)や connSupervisionTimeout (接続が失われたと判断する、コネクション・イベントの失敗回数)の条件式も明示されているので、どういう条件で接続が失われるのか、を具体的に把握する指針となり、参考になります。


参考:BLEの通信仕様 - Reinforce-Lab.’s Blog


Service Changed Characteristic

そのアクセサリがサービスを変更する可能性のある場合(つまり、GATT を変更する可能性がある場合)は "Service Changed Characteristic" を実装しましょう、と書かれています。

The accessory shall implement the Service Changed characteristic only if the accessory has the ability to change

its services during its lifetime.


アップル製品は、前回読んだ(キャッシュした)情報に依るかどうかを "Service Changed Characteristic" を使って決めますよ、と。

The Apple product may use the Service Changed characteristic to determine if it can rely on previously read

(cached) information from the device.


これの話ですね。


Core Bluetooth でいうと、サービス変更時に CBPeripheralDelegate の `peripheral:didModifyServices:` が呼ばれる、という件ですが、そもそもアクセサリ側で "Service Changed Characteristic" が実装されている必要があります。(実装されてなければ iOS の Bluetooth 設定を off/on してキャッシュクリアする)


Siri

「カスタム Siri コマンドを有効にする」「Siri のアベイラビリティ情報を取得する」「Siri セッションの初期化」「Siri Eyes Free Mode」「Siriセッションを最適化する」などなど興味を惹かれるタイトルが並んでいますが、HFP プロファイルとかのクラシック Bluetooth の話で Core Bluetooth でどうこうできる範囲ではないのでスルー。


「6.5 Improving Voice Recognition」も iOS エンジニアがどうこうできる部分ではないが、S/N比の推奨は 20 dB 以上 とか書いてあって、Siri とか関係なく、良好な音声入力の基準値として今後 iOS で音声処理やる際の改善のヒントになりそうです。


関連

上原さんのブログにBLE部分を日本語訳したものがまとめられています。


2009 | 08 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2013 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2014 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |