Flutter Desktopで簡易アノテーションツールを作る

# はじめに

新しく画像分類モデルを作成したいと思ったのですが、前回、学習データを作成したときは画像に対するラベリングをMac標準のFinderを使いドラッグアンドドロップ操作で行ったため、手が疲れました。そこで今回は画像に対する手動のラベリングを疲れにくい操作で行えるツールをFlutterで新たに作成しました。

![annotation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/cf3d5164-d479-f832-55c2-8a85be2a827b.gif)

ソースコード [tfandkusu/ikut_annotation](https://github.com/tfandkusu/ikut_annotation)

## 前回の作成の様子

[スプラトゥーン2のプレイ動画から、やられたシーンだけをディープラーニングで自動抽出する](https://qiita.com/tfandkusu/items/acbc0906046bb7b2b1db)

## なぜFlutter Desktopか

- 学習のための画像約5万枚が手元のPCにあり、デスクトップアプリならば他に動かさなくて良いです。
- タッチパネルよりキーボードの方が速く操作できそうです。
- ブラウザで動くものだとサーバーサイドアプリが必要で工数が大きいです。
- 個人的にデスクトップアプリを作れる手段としてFlutterが一番馴染んでいました。


# ツールの要件

私が作った画像分類モデルはスプラトゥーン2のシーン分類でしたが、プレイヤー以外には分かりにくいので、今回は[Food 101データセット](https://data.vision.ee.ethz.ch/cvl/datasets_extra/food-101/)をお借りして、料理画像の分類モデルのための学習データを作成するためのツールを作成します。


## ラベル

今回は画像に

- takoyaki
- sushi
- gyoza
- other (ラベリング不可能)

の何れかのラベルを設定します。

ラベル一覧はlabel.txtファイルに設定します。

```label.txt
takoyaki
sushi
gyoza
other
```

## ラベル付き画像

画像は実行ファイル直下のimageに置き、result.csvにファイル名とラベル名を設定します。後述する操作で、このCSVの2列目は書き換わります。

```result.csv
1003289.jpg,takoyaki
100332.jpg,sushi
1003912.jpg,gyoza
1005352.jpg,sushi
1009005.jpg,gyoza
1010258.jpg,takoyaki
1012499.jpg,sushi
1012802.jpg,takoyaki
1013418.jpg,sushi
101625.jpg,sushi
```

## 操作方法

キーボードで操作します。

| 操作 | キー |
|---|---|
| 次の画像を表示/選択する | ]キー |
| 前の画像を表示/選択する | [キー |
| 選択画像にラベル1を設定する | Zキー |
| 選択画像にラベル2を設定する | Xキー |
| 選択画像にラベル3を設定する | Cキー |
| 選択画像にラベル4を設定する | Vキー |
| 100枚先の画像を表示/選択する | Pキー |
| 100枚前の画像を表示/選択する | Oキー |

# デバッグ実行する

ほぼ公式の[Desktop support for Flutter](https://flutter.dev/desktop)に記載されている内容です。

## Macでデバッグ実行する

(インストールされていなければ)[Xcode](https://developer.apple.com/xcode/)と[CocoaPods](https://cocoapods.org/)をインストールします。

Flutter SDKに対してMacOSを有効可します。

```sh
flutter config --enable-macos-desktop
```

プロジェクトに対してMacOS用のビルドをできるようにします。

```sh
flutter create --platforms=macos .
```

Android StudioからmacOS(desktop)を選んで起動します。

![スクリーンショット 2021-03-08 5.31.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/564be58e-ebd6-9cab-7165-fb7688532d48.png)

または

```sh
flutter run -d mac
```

コマンドで実行します。

### app-sandboxの無効化

AppStoreで配布されるアプリがローカルファイルの読み書きをするためには、権限の管理が必要です。しかし今回はAppStoreで配布しないので、app-sandboxは無効化して、特に権限の管理は行いません。

```xml:macos/Runnner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 
	<key>com.apple.security.app-sandbox</key>
	<true/>
    -->
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
</dict>
</plist>
```

#### 参考

[Entitlements and the App Sandbox](https://flutter.dev/desktop#entitlements-and-the-app-sandbox)
[App Sandbox Entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_app-sandbox)


## Windowsでデバッグ実行する

Windowsでデバッグ実行および後述する配布用ビルドを作成するときは、Windows PCで行う必要があります。

[Visual Studio 2019](https://visualstudio.microsoft.com/ja/downloads/)をインストールします。

Flutter SDKに対してWindowsを有効可します。

```sh
flutter config --enable-windows-desktop
```

プロジェクトに対してWindows用のビルドをできるようにします。

```sh
flutter create --platforms=windows .
```

Android StudioからWindows(desktop)を選んで起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/4ffb51a4-575b-5c85-413f-443ba29d3c8d.png)

または

```sh
flutter run -d windows
```

コマンドで実行します。

# Windows向け配布用ビルドを作成する

Windows向けに配布用ビルドを作成しました。作成はWindows PC上で行う必要があります。


```sh
flutter build windows
```

ちなみに、普段Macで開発しているのでクロスコンパイルができると便利なのですが、Macでビルドしようとすると、このようなメッセージが出ます。

```
"build windows" only supported on Windows hosts.
```

配布用ビルド作成結果はプロジェクト直下の build/windows/runner/Release ディレクトリに作成されます。flutter_windows.dll も data ディレクトリも必要な物です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/2bb744fb-f496-443e-bc3c-bbd2d8112a5b.png)

それらのファイルに依存するdllファイルと、アプリが必要とするファイルやディレクトリを合わせて、配布パッケージとします。


- 依存するdll
    - msvcrp140.dll
    - vcruntime140.dll
    - vcruntime140_1.dll
- アプリが必要とするファイルやディレクトリ
    - image/ 画像ディレクトリ
    - label.txt ラベル一覧
    - result.csv ラベル付け結果


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/20a664be-7187-777d-7b76-574995c24c71.png)

依存するdllファイルはVisual Studio 2019のインストールディレクトリからコピーします。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/3f08ba9c-76be-a926-de8b-335f78cc7164.png)


[マイクロソフト公式情報](https://docs.microsoft.com/en-us/cpp/windows/walkthrough-deploying-a-visual-cpp-application-to-an-application-local-folder?view=msvc-160)

# プログラムの解説

今回作成したFlutterプロジェクトはこちらで公開されています。

https://github.com/tfandkusu/ikut_annotation

この節ではデスクトップアプリならではの部分をかいつまんで紹介します。

## ローカルファイルの読み書き

ローカルファイルの読み書きには[File](https://api.flutter.dev/flutter/dart-io/File-class.html)クラスを使用しました。また、CSVの読み書きには[csv](https://pub.dev/packages/csv)パッケージを利用しました。

label.txtファイルからラベル一覧を取得するコードはこのようになりました。

```main_state_notifier_provider.dart
Future<List<String>> _loadLabels() async {
  final dir = Directory.current.path;
  final file = new File('$dir/label.txt');
  // ファイルを読む
  final csvString = await file.readAsString();
  // CSVと解釈して各行をList<String>に格納する。
  final fields = CsvToListConverter().convert(csvString);
  final labels = fields.map((items) => items[0] as String).toList();
  return labels;
}
```

ラベル付けの結果をCSVファイルに書き出すコードはこのようになりました。

```main_state_notifier_provider.dart
// CSV文字列を得る
final csvString = ListToCsvConverter().convert(state.images.map((image) {
  // 二次元配列に変換する
  final name = basename(image.path);
  return [name, image.label];
}).toList());
_file.writeAsString(csvString);
```

## キーボード入力

キーボード入力はこのように[Focus](https://api.flutter.dev/flutter/widgets/Focus-class.html)を使用して取得します。

```main.dart
class MainPage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final mainStateNotifier = useProvider(mainStateNotifierProvider);
    return Focus(
      autofocus: true,
      onKey: (node, event) {
        if (event is RawKeyDownEvent) {
          if (event.logicalKey.keyLabel == ']') {
            mainStateNotifier.move(1);
          } else if (event.logicalKey.keyLabel == '[') {
            mainStateNotifier.move(-1);
          } else if (event.logicalKey.keyLabel == 'p') {
            mainStateNotifier.move(100);
          } else if (event.logicalKey.keyLabel == 'o') {
            mainStateNotifier.move(-100);
          } else if (event.logicalKey.keyLabel == 'z') {
            mainStateNotifier.update(0);
          } else if (event.logicalKey.keyLabel == 'x') {
            mainStateNotifier.update(1);
          } else if (event.logicalKey.keyLabel == 'c') {
            mainStateNotifier.update(2);
          } else if (event.logicalKey.keyLabel == 'v') {
            mainStateNotifier.update(3);
          }
        }
        // ここをfalseにすると、MacOSでペコペコ音が鳴る
        return true;
      },
      child: Scaffold(
          appBar: AppBar(
            title: Text('iKut annotation'),
          ),
          body: Stack(children: [
            _buildPreviousImage(),
            _buildImage(),
            _buildLabels()
          ])),
    );
  }
  // 略
}
```

# まとめ

FlutterでスマホやWebのアプリを作っていて、機械学習のための学習データ作成のように、開発のためのツールが必要になったときは、Flutter DesktopでPC向けのアプリケーションを作成する選択肢もあると思いました。