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は、この時点では、渡されたファイルの中身を表示するだけです。
そちらは次回に見ていきます。