Flutter で画像認識
Flutterでカメラにアクセスして、AI (Machine Learning) で画像認識などしてみようと思います.
まずは、普通にカメラの画像を撮影するまで.
今回は、普通にスマホのカメラで写真を撮るところまでを実装します。
例によって、ソースコードは以下にあります。
Github - flutter_read_thermo の basic_camera
Flutterのサンプル に詳しい説明が載ってます。 カメラで写真を撮るところまでは、そちらも参照に。サンプルとは、所々違うところがあります。
flutter create
でプロジェクトを作ったら、カメラのハードウェアにアクセスするための権限設定をします。
当然カメラがないと動作確認できないので、実機での開発をお勧めします。
android/app/build.gradle
を編集して、minSdkVersion
を 21以上にします。
defaultConfig {
...
minSdkVersion 21
...
}
ios/Runner/Info.plist
にカメラアクセス用の権限リクエストを追加します。
<dict>
...
<key>NSCameraUsageDescription</key>
<string>写真をとります</string>
</dict>
あと、iOSでは通常の Flutter アプリと同じで、XCodeを使ってSigning
とBundle ID
の変更が必要です。
カメラのハードウェアを使うには、別途パッケージのインストールが必要となります。
Flutterでは、Flutterのコアチームを始め、色々なデベロッパーがライブラリ類をパッケージとして公開してくれています。 ほとんどが、Android と同じで、BSD ライセンスです。
pub.dev - flutterパッケージの公開リポジトリ
サーチすると様々なパッケージがあると思いますので、汎用的なもので、みんなが使うだろう機能は、作る前にここを探してみると二度手間が省けたりします。
普通の Flutter アプリは、main()
から、runApp()
を呼んで、アプリ Widget を渡すだけです。
今回は、アプリの起動前に初期化処理を行うので、main()
を非同期実行します。
カメラのハードウェア初期化は実行に時間がかかるために非同期にします。
Future<void> main() async {
try {
WidgetsFlutterBinding.ensureInitialized();
final cameras = await availableCameras();
final firstCamera = cameras.first;
runApp(MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CameraView(camera: firstCamera),
));
} on CameraException catch (e) {
debugPrint('Initialize Error: ${e.description}');
}
}
Dart
で非同期実行にするには、async
キーワードを関数に付けます。
availableCameras()
の戻り値は、Future<List<CameraDescription>>
です。
Future<T>
は、なんらかの処理を非同期で実行して、終了時、もしくはエラー時に実行されるコールバックを登録できるようにするものです。
Futureにawait
すると、結果が出るまで処理を待って、その値を受け取ることができるようになります。
ただし、await
するためには、それを呼んでいる関数全体をasync
しなければいけません。
カメラは複数存在する機器もあるので、List
の中の最初のエントリfirst
を使う様にします。
(今回はテスト実装なので決め打ちにしてます)
CameraView
は、カメラ画像のプレビュー表示をして、ボタンが押されたら写真を撮る機能です。
StatefulWidget
にして、コンストラクタに渡すcamera
の状態に反応するようにします。
class CameraView extends StatefulWidget {
final CameraDescription camera;
const CameraView({
Key? key,
required this.camera,
}) : super(key: key);
@override
_CameraViewState createState() => _CameraViewState();
}
CameraViewのStateには、_controller
と_initializeControllerFuture
を持たせます。
class _CameraViewState extends State<CameraView> {
late final CameraController _controller;
late final Future<void> _initializeControllerFuture;
// Initializes camera controller to preview on screen
void _initializeCamera() async {
final CameraController cameraController = CameraController(
widget.camera,
ResolutionPreset.high,
enableAudio: false,
);
_controller = cameraController;
_initializeControllerFuture = _controller.initialize();
}
@override
void initState() {
_initializeCamera();
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
_controller
は、カメラハードウェアの制御をするためのクラスです。
_initializeControllerFuture
は、カメラの初期化を実行するためのFuture
です。
カメラ自体の初期化には時間がかかるので、初期化終了時の処理は非同期実行になります。
initState()
は、このState
が初期化された時、dispose()
は、終わりに呼び出されます。
そこで、カメラのハードウェアの初期化を行っています。
UIツリーの方ですが、Scaffold
は画面要素を持った枠組みで、appBar
、body
、floatingActionButton
などのプロパティを設定するとUIのデザインに合わせて適当に配置してくれます。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Camera Preview'),
),
FutureBuilder
は、future:
に指定したFuture
が終了した後に、builder:
が実行されます。
(厳密に言うとちょっと違うのですが、使い方としてはそんな感じです。)
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Center(child: CameraPreview(_controller));
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
最初は、ConnectionState
がwaiting
になるので、CircularProgressIndicator
を中央に表示します。
_initializeControllerFuture
は、カメラが使える様になったら complete します。
CameraPreview
でリアルタイムのプレビュー画面に変えます。
最後は、floatingActionButton
の設定で、画面の下の方に浮き上がる形のボタンを配置します。
ボタンが押されると、onPressed()
が呼び出されます。
floatingActionButton: FloatingActionButton(
onPressed: () async {
try {
await _initializeControllerFuture;
final image = await _controller.takePicture();
debugPrint('Picture: ${image.path}');
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetectView(
imagePath: image.path,
),
),
);
} catch (e) {
// If an error occurs, log the error to the console.
debugPrint('Picture Error: Error: ${e}');
}
},
child: const Icon(Icons.camera_alt),
));
}
_controller.takePicture()
が写真の撮影です。Future<XFile>
が返ってきます。
XFile
は、カメラ画像を一時ファイルに保存したものです。
Navigator.of(context).push()
は詳しくは触れませんが、画面遷移を実行します。
push
すると画面スタックに現在の画面を積んで、その上に引数で与えた画面を描画することで、その画面に遷移します。
ここでは、DetectView
に、そのファイルのパス名を渡しています。
DetectView
は、この時点では、渡されたファイルの中身を表示するだけです。
そちらは次回に見ていきます。