はじめに
この記事ではFlutterでGoogleのML Kitのポーズ推定を使ったアプリのサンプルコードの紹介と解説をしています。動作確認はiOSシミュレーターのみです。実機やAndroidでもコードの大部分はそのまま動かせると思いますが、WebとPCではML Kitが未対応なのでご注意ください。
ソースコードはコチラでご覧ください。(ほぼ全てのコードはこの記事中でも見られます)
動画の処理にはFFmpegを利用します。
表題のML KitとFFmpegの利用箇所は5. mlkit_video_converter.dartについて
で解説しています。
機能について
実装機能について、大枠は以下の通りです。
- iOSのカメラロールの動画を取得
- FFmpegで動画をフレーム(コマ送りの画像)に分割
- ML KItでポーズ推定しその結果をフレーム上に描画
- FFmpegでフレームを再合成して動画を作成
- 作成した動画をカメラロールに保存
環境と利用パッケージ
macOS : 13.0.1
Flutter : 3.3.9
Xcode : 14.1
Simulator : iOS 16.1
- google_mlkit_pose_detection: 0.5.0:ML KitのFlutterパッケージ
- ffmpeg_kit_flutter: 4.5.1-LTS:FFmpeg (投稿時点でも最新版ではないので注意)
- image_picker: 0.8.6:カメラロールから画像を取得
- path_provider: 2.0.11:端末のキャッシュ用ディレクトリを取得
- flutter_video_info: 1.3.1:選択した動画のピクセルサイズやFPSを取得
- image_gallery_saver: 1.7.1:生成した動画をカメラロールに保存
ML KitとFFmpegについて
ML Kit
Google製のモバイル用SDKで、TensorFlowという機械学習のソフトウェアライブラリをより手軽に使えるようにしてくれるものです。
文字認識や顔認識などの用途別にパッケージが用意されており、それぞれに必要な学習済みモデルがラップされています。今回はそのなかでもポーズ推定用のFlutterパッケージを利用します。
FFmpeg
動画と音声を記録・変換・再生するためのフリーのコマンドツールです。
FFmpegのコマンドをFlutterアプリから実行できるようにしてくれるパッケージがffmpeg_kit_flutterです。
記事投稿時の環境では最新版の5.1.0
や5.1.0-LTS
はアプリが起動すらしないという事象がおこり、原因がわからなかったため、今回は4.5.1-LTS
を利用しています。
※ FFmpeg公式のMini FAQに特許周りの大事なお話が書かれているので、利用の際はご注意ください。
フォルダ構成について
Flutterプロジェクトのlib
以下のフォルダは以下の通りです。
- main.dart : 説明割愛
- view
- main_screen.dart : このアプリの唯一のスクリーン
- video_convert_view.dart : 名前はViewですがViewModel的に利用
- utility
- utilities.dart : ちょっとしたメソッドをいくつか
- model
- landmark_painter.dart : ポーズ推定結果を描画するCustomPainter
- mlkit_video_converter.dart : 動画や画像の編集を行うクラス
- ファイル数やコードの行数を抑えることを優先して、
video_convert_view.dart
内のWidgetにロジックを含めてしまっています。 - 実際にアプリの一部として機能実装する場合は、MVVMなどのパターンを採用する方がよいでしょう。その場合はriverpodなどを利用するのがオススメです。
- メソッドや処理の部分を参考にご覧ください。
1. プロジェクトの準備
ここから実際にFlutterプロジェクトの解説をしていきます。
1.1 プロジェクト作成
flutter create --platform=ios your_project_name
Flutterコマンドラインで作成する場合は--platform
オプションで対応プラットフォームを指定することで、不要なプラットフォームを削除する手間が省けます。
1.2 ARMv7の除外
ML KitはARMv7アーキテクチャのCPUに対応していないため、プロジェクトを作成したらまずはこちらの解説に従い、ARMv7を明示的に除外します。方法は以下のaとbの2つがあります。
(全く同じ設定の内容ではありませんが、bのコードは解説の引用です)
1.2.a Xcodeで行う
ios
ディレクトリをXcodeで開きProject > Runner > Building Settings > Excluded Architectures
にAny SDK > armv7
を追加する。
1.2.b Podfile
を編集する
ios/Podfile
に下記コードを追記する。(Podfileがない場合はios
ディレクトリでpod init
)
解説に合わせてiOSVersion = '10.0'
としていますが、他に利用するパッケージが新しいiOSバージョンにしか対応してない場合は、適宜変更する必要があると思います。
# ...省略...
# add this line:
$iOSVersion = '10.0'
post_install do |installer|
# add these lines:
installer.pods_project.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=*]"] = "armv7"
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion
end
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# add these lines:
target.build_configurations.each do |config|
if Gem::Version.new($iOSVersion) > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'])
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = $iOSVersion
end
end
end
end
1.3 カメラロール利用の許可設定
カメラロールを利用する旨をユーザーに伝えるための設定をします。
(これをしないと、カメラロールにアクセスしようとしただけでアプリが強制終了します)
こちらも方法は以下のaとbの2つがあります。
1.3.a Xcodeで行う
ios
ディレクトリをXcodeで開きRunner > Runner > Info.plist
にPrivacy - Photo Library Usage Description
を追加する。
Valueにはカメラロールにアクセスする理由をユーザーにわかる様に記載してください。
1.3.b Info.plist
を編集する
/ios/Runner/Info.plist
にNSPhotoLibraryUsageDescription
を追記する。
<?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>NSPhotoLibraryUsageDescription</key>
<string>ビデオファイルの取り込みを保存をします。</string>
</dict>
</plist>
1.4 パッケージの追加
先述の通りffmpeg_kit_flutter
だけは最新版ではないものを利用していますので、ご注意ください。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
# 以下追記
google_mlkit_pose_detection: ^0.5.0
image_picker: ^0.8.6
path_provider: ^2.0.11
ffmpeg_kit_flutter: 4.5.1-LTS
flutter_video_info: ^1.3.1
image_gallery_saver: ^1.7.1
pubspec.yaml
に追記したらflutter pub get
を実行し、パッケージのダウンロードをしてください。
2. main.dart
について
特筆することはありませんが、debugShowCheckedModeBanner: false
とtheme: ThemeData(primarySwatch: Colors.pink)
でテーマカラーをお好みで変更すると、開発が少し楽しくなるのでオススメです。
import 'package:flutter/material.dart';
import 'package:flutter_mlkit_video/view/main_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // アプリ内のdebug表記をなくす
title: 'Google MLKit Demo',
theme: ThemeData(primarySwatch: Colors.pink), // お好みのカラーに変更
home: const MainScreen(), // このアプリで唯一の画面
);
}
}
3. main_screen.dart
について
- ビデオファイルを選択する画面です。
- できるだけ少ない画面とファイルで機能実装をしているため、少し歪な実装になっています。
操作と処理の流れ
-
_videoPicked = null
の初期状態ではscaffoldBody
は「ファイル選択」ボタンを表示 - ファイルを選択すると
XFile
形式で_videoPicked
に格納しsetState
で再描画 -
scaffoldBody
が、次の画面に当たるVideoConvertView
に切り替わる
import 'package:flutter/material.dart';
import 'package:flutter_mlkit_video/view/video_convert_view.dart';
import 'package:image_picker/image_picker.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
XFile? _videoPicked; // image_pickerで選択したビデオファイル
late Widget scaffoldBody; // 表示するWidget、_videoPickedに応じて変化させる
// ファイル選択ボタン押下
Future<void> _pickVideo() async {
// カメラロールからビデオファイルを選択
await ImagePicker().pickVideo(source: ImageSource.gallery).then((result) {
if (result != null) {
_videoPicked = result;
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
// ファイル選択状況に応じてビューを切り替え
if (_videoPicked == null) {
// ファイル未選択時は「ファイル選択」ボタンを表示
scaffoldBody = Container(
alignment: Alignment.center,
child: ElevatedButton(
child: const Text('ファイル選択'),
onPressed: () async => _pickVideo(),
),
);
} else {
// ファイル選択後はポーズ推定を行うビューを表示
scaffoldBody = VideoConvertView(videoXFile: _videoPicked);
}
return Scaffold(
appBar: AppBar(title: const Text('Flutter ML Kit')),
body: SafeArea(child: scaffoldBody),
);
}
}
4. video_convert_view.dart
について
- ビデオの処理開始後に
Scaffold
のbody
に表示されるビューです。 -
view
という名前ですがファイル数を減らすためにロジックも含めています
(コードが胴長で、デザインとしては好ましくないと思います。ViewModelなどのロジックを担当するclassを作るのがよいでしょう。)
表示と処理の流れ
- 親ビューから
videoXFile
を受け取る -
init()
でポーズ推定の処理を開始し、Future<void>
型の戻り値を_future
に渡す -
_future
の処理状況に応じてFutureBuilder
内snapshot
のConnectionState
が変化する -
snapshot
のConnectionState
に応じての表示が切り替わる-
ConnectionState.done
より前:プログレスサークル表示 -
ConnectionState.done
後:「カメラロールに保存しました」表示 -
ConnectionState
について詳しくはこちら
-
_convertVideo()
メソッドについて
- ポーズ推定からカメラロールへの保存までのメソッド群を非同期で実行するメソッドです。
-
(1)
MlkitVideoConverter
はカスタムクラスで、ML kit
やFFmpeg
を使った実際の処理を行います。詳細は後述します。 -
removeFFmpegFiles()
やlocalFilePath()
などのメソッドは後述するutilities.dart
に記述されています。詳細は後述しますが機能については下記コメントの通りです。 -
(2)
中頃のfor
ループでは、動画から取得した各フレームで順番にポーズ推定し、結果を画像の上に描画する処理を行なっています。 -
(3)
進捗率を表す_progress
は、実際には(2)
のforループ
で完了した処理の数を元にしています。
import 'package:flutter/material.dart';
import 'package:flutter_mlkit_video/model/mlkit_video_converter.dart';
import 'package:flutter_mlkit_video/utility/utilities.dart';
import 'package:image_picker/image_picker.dart';
class VideoConvertView extends StatefulWidget {
const VideoConvertView({super.key, this.videoXFile});
final XFile? videoXFile; // 選択したビデオファイル
@override
State<VideoConvertView> createState() => _VideoConvertViewState();
}
class _VideoConvertViewState extends State<VideoConvertView> {
late Future<void> _future; // FutureBuilderに動画処理完了を通知する
var _busy = false; // 動画処理実行中ガード
var _progress = 0.0; // (4) ポーズ推定の進捗率
// ビデオの全フレームにランドマークを描画してカメラロールに保存
Future<void> _convertVideo() async {
if (!_busy) {
// 開始
setState(() => _busy = true);
// 選択したファイルパス
final videoFilePath = widget.videoXFile?.path;
// ファイル未選択時ガード
if (videoFilePath == null) return;
// 作成したファイルの保存先パス
final localPath = await localFilePath();
// (1)
// コンバーターの作成と初期化
final mlkitVideoConverter = MlkitVideoConverter();
await mlkitVideoConverter.initialize(
localPath: localPath,
videoFilePath: videoFilePath,
);
// フレーム抽出
final frameImageFiles = await mlkitVideoConverter.convertVideoToFrames(context: context);
// 全フレームにランドマークを描画
if (frameImageFiles != null) {
// (2)
for (var index = 0; index < frameImageFiles.length; index++) {
final file = frameImageFiles[index];
await mlkitVideoConverter.paintLandmarks(context: context, frameFilePath: file.path);
// (3)
setState(() => _progress = index / frameImageFiles.length); // プログレス更新
}
}
// フレームから動画生成
final exportFilePath = await mlkitVideoConverter.createVideoFromFrames();
// カメラロールに保存
if (exportFilePath != null) {
await ImageGallerySaver.saveFile(exportFilePath);
}
// キャッシュクリア
await removeFFmpegFiles();
// 完了ダイアログ表示
showDialog(
context: context,
builder: (_) {
return AlertDialog(
content: const Text('カメラロールに保存しました'),
actions: [
TextButton(child: const Text('OK'), onPressed: () => Navigator.pop(context)),
],
);
},
);
// 終了
setState(() => _busy = false);
}
}
// 処理中プログレスバー
Widget _progressView(double value) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('書き出し中'),
const SizedBox(height: 16),
CircularProgressIndicator(
value: value,
backgroundColor: Colors.black12,
),
],
);
}
@override
void initState() {
super.initState();
// ポーズ推定開始
_future = _convertVideo();
}
@override
Widget build(BuildContext context) {
return widget.videoXFile == null
// ---------- ファイル選択前 ----------
? Container(
alignment: Alignment.center,
child: const Text('ファイルを選択してください'),
)
// ---------- ファイル選択後 ----------
: FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// ---------- 処理中 ----------
return Container(
alignment: Alignment.center,
child: _progressView(_progress),
);
} else if (snapshot.hasError) {
// ---------- エラー発生時 ----------
return Container(
alignment: Alignment.center,
child: Text(snapshot.error.toString()),
);
} else {
// ---------- 完了 ----------
return Container(
alignment: Alignment.center,
child: const Text('カメラロールに保存しました'),
);
}
},
);
}
}
5. mlkit_video_converter.dart
について
-
MlkitVideoConverter
はビデオのフレーム抽出、ポーズ推定処理、フレームからのビデオの再生成などの処理を行うメソッド群を持つカスタムクラスです。 - タイトルのML KitやFFmpegはこの
MlkitVideoConverter
内で利用しています。
各メソッドについて
-
initialize()
- インスタンス宣言直後に実行して、必要なビデオのメタデータを取得しておきます。
-
ffmpegCoomand
はFFmpegで実行する画像処理コマンドで、オプションの詳細は以下の通りです。-
-i $videoFilePath
:入力ファイルのパスを指定 -
-vcodec png
:出力ファイルの形式pngに指定 -
$localPath/${CommonValue.filePrefix}%05d.png
出力ファイルのパスとファイル名を指定 (%05d
で5桁の連番が自動で付与される)
-
-
convertVideoToFrames()
- ビデオを全フレームを画像として取得し、デバイスのキャッシュフォルダに保存します。
-
_getFFmpegFiles()
-
convertVideoToFrames()
で生成した画像の一覧を取得します。
-
-
paintLandmarks()
- ポーズ推定をしてランドマーク (関節や目口鼻の位置) の座標を取得し、画像に描画します。
-
createVideoFromFrames()
- ポーズ推定と描画が終わった画像を全て再合成して動画を出力します。
-
ffmpegCoomand
はFFmpegで実行する画像処理コマンドです。-
-i $localPath/${CommonValue.filePrefix}%05d.png
:入力ファイルのパス ($localPath/
下でファイル名が5桁の連番になっているpng画像全て) -
-framerate $videoFps
:元動画のfps -
-b 100M
:出力動画のbps (劣化させないように必要より大きな値を指定しています) -
-r $videoFps
:出力動画のfps -
$exportVideoFilePath
:出力動画のパス
-
import 'dart:io';
import 'dart:ui' as ui;
import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter/return_code.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mlkit_video/model/landmark_painter.dart';
import 'package:flutter_mlkit_video/utility/utilities.dart';
import 'package:google_mlkit_pose_detection/google_mlkit_pose_detection.dart';
class MlkitVideoConverter {
late final String localPath;
late final String videoFilePath;
late final int videoWidth;
late final int videoHeight;
late final double videoFps;
// メタデータ取得などの初期化処理
Future<void> initialize({
required String localPath,
required String videoFilePath,
}) async {
final Map<String, dynamic>? videoInfo = await getVideoMetadata(videoFilePath);
this.localPath = localPath;
this.videoFilePath = videoFilePath;
videoWidth = videoInfo!['width'];
videoHeight = videoInfo['height'];
videoFps = videoInfo['fps'];
}
// ビデオをフレームに分割してPNG画像として保存
Future<List<File>?> convertVideoToFrames() async {
await removeFFmpegFiles();
// フレーム抽出
final ffmpegCoomand =
'-i $videoFilePath -vcodec png $localPath/${CommonValue.filePrefix}%05d.png';
await FFmpegKit.execute(ffmpegCoomand).then((session) async {
final returnCode = await session.getReturnCode();
// エラーまたは中断
if (ReturnCode.isCancel(returnCode) || !ReturnCode.isSuccess(returnCode)) {
return null;
}
}).onError((error, stackTrace) {
return null;
});
return _getFFmpegFiles();
}
// convertVideoToFrames()で生成した画像のリストを取得
List<File> _getFFmpegFiles() {
List<File> files = [];
final localDirectory = Directory(localPath);
List<FileSystemEntity> fileEntities =
localDirectory.listSync(recursive: true, followLinks: false);
for (var entity in fileEntities) {
final fileName = entity.path.split('/').last;
if (fileName.startsWith(CommonValue.filePrefix)) {
files.add(File(entity.path));
}
}
return files;
}
// フレームにポーズ推定結果を描画してに上書き保存
Future<bool> paintLandmarks({required String frameFileDirPath}) async {
// ファイル
final imageFile = File(frameFileDirPath);
if (imageFile.existsSync()) {
// ボーズ推定
final inputImage = InputImage.fromFile(imageFile);
final poseDetector = PoseDetector(options: PoseDetectorOptions());
await poseDetector.processImage(inputImage).then((value) async {
final pose = value.first;
// 画像のデコード
final imageByte = await imageFile.readAsBytes();
final image = await decodeImageFromList(imageByte);
// キャンバス上でランドマークの描画
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final painter = LankmarkPainter(image: image, pose: pose);
painter.paint(canvas, Size(videoWidth.toDouble(), videoHeight.toDouble()));
// ランドマーク付き画像ByteData生成
final ui.Picture picture = recorder.endRecording();
final ui.Image imageRecorded = await picture.toImage(videoWidth, videoHeight);
final ByteData? byteData = await imageRecorded.toByteData(format: ui.ImageByteFormat.png);
// 上書き保存
await File(imageFile.path).writeAsBytes(byteData!.buffer.asInt8List());
});
return true;
} else {
return false;
}
}
// フレームから動画を再生成
Future<String?> createVideoFromFrames() async {
final exportVideoFilePath = '$localPath/ffmpeg_video.mp4';
final ffmpegCommand =
'-framerate $videoFps -i $localPath/${CommonValue.filePrefix}%05d.png -b 100M -r $videoFps $exportVideoFilePath';
var succeed = false;
await FFmpegKit.execute(ffmpegCommand).then((session) async {
final returnCode = await session.getReturnCode();
succeed = ReturnCode.isSuccess(returnCode);
});
if (succeed) {
return exportVideoFilePath;
} else {
return null;
}
}
}
6. landmark_painter.dart
について
- ビデオから取得したフレーム画像にポーズ推定の結果 (ランドマークの位置) を描画する処理を行う
CustomPainter
です。
ランドマークについて詳細はこちらをご覧ください - 左腕 > 右腕 > 左脚 > 右脚と順に同じ描画の処理をするので、ここでは省略して記載しています。
-
drawCircle()
でランドマーク上に点を描き、drawLine()
でランドマークを結ぶ線を描画します。
簡単な解説
-
(1)
for
ループで便利のため、最大32個あるランドマークを顔と左右の手脚に分類しておく。 -
(2)
canvasにポーズ推定した画像を描画 (この上にランドマークを描画していく) -
(3)
顔のランドマークを画像に描画 -
(4)
左右の手脚の付け根4点をグレーのラインで結ぶ -
(5)
左腕のランドマークを順に描画し線で結ぶ (以下、右腕と脚を同様のため中略) -
(6)
便利のため32個のPoseLandmarkType
に全て色を割り振っておく
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_mlkit_pose_detection/google_mlkit_pose_detection.dart';
class LankmarkPainter extends CustomPainter {
LankmarkPainter({
required this.image,
required this.pose,
});
final ui.Image image; // MK kitへの入力画像
final Pose pose; // ML kitの出力
// (1) ランドマークを分類
List<PoseLandmarkType> get faceLandmarks => [
PoseLandmarkType.leftEyeInner,
PoseLandmarkType.leftEye,
PoseLandmarkType.leftEyeOuter,
PoseLandmarkType.rightEyeInner,
PoseLandmarkType.rightEye,
PoseLandmarkType.rightEyeOuter,
PoseLandmarkType.leftEar,
PoseLandmarkType.rightEar,
PoseLandmarkType.leftMouth,
PoseLandmarkType.rightMouth,
];
List<PoseLandmarkType> get rightArmLandmarks => [
PoseLandmarkType.rightShoulder,
PoseLandmarkType.rightElbow,
PoseLandmarkType.rightWrist,
PoseLandmarkType.rightThumb,
PoseLandmarkType.rightIndex,
PoseLandmarkType.rightPinky,
];
// ...中略...
@override
void paint(canvas, size) async {
const strokeWidth = 4.0;
// (2)
// 元画像の描画
canvas.drawImage(image, Offset.zero, Paint());
// ランドマークの描画
// (3)
// 顔
for (var landmark in faceLandmarks) {
final paint = Paint()..color = landmark.color;
final position = Offset(pose.landmarks[landmark]!.x, pose.landmarks[landmark]!.y);
canvas.drawCircle(position, strokeWidth, paint);
}
// (4)
// 胴体
final paint = Paint()
..color = Colors.grey
..strokeWidth = strokeWidth;
// 左右の方と脚の付け根の座標
final p1 = Offset(
pose.landmarks[rightArmLandmarks.first]!.x, pose.landmarks[rightArmLandmarks.first]!.y);
final p2 = Offset(
pose.landmarks[leftArmLandmarks.first]!.x, pose.landmarks[leftArmLandmarks.first]!.y);
final p3 = Offset(
pose.landmarks[leftLegLandmarks.first]!.x, pose.landmarks[leftLegLandmarks.first]!.y);
final p4 = Offset(pose.landmarks[rightLeg.first]!.x, pose.landmarks[rightLeg.first]!.y);
// 4点をつなぐ直線を描画
canvas.drawLine(p1, p2, paint);
canvas.drawLine(p2, p3, paint);
canvas.drawLine(p3, p4, paint);
canvas.drawLine(p4, p1, paint);
// (5)
// 左腕の関節を順に描き線で結ぶ
for (var index = 0; index < leftArmLandmarks.length - 1; index++) {
final landmark1 = leftArmLandmarks[index];
final landmark2 = leftArmLandmarks[index + 1];
final paint = Paint()
..color = landmark1.color
..strokeWidth = strokeWidth;
final p1 = Offset(pose.landmarks[landmark1]!.x, pose.landmarks[landmark1]!.y);
final p2 = Offset(pose.landmarks[landmark2]!.x, pose.landmarks[landmark2]!.y);
canvas.drawCircle(p1, strokeWidth, paint);
if (index < leftArmLandmarks.length - 1) {
canvas.drawLine(p1, p2, paint);
}
}
//...中略...
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
// (6)
// ランドマークごとに色を割り当てるextension
extension PoseLandmarkColor on PoseLandmarkType {
Color get color {
if (this == PoseLandmarkType.rightHip ||
this == PoseLandmarkType.rightKnee ||
this == PoseLandmarkType.rightAnkle ||
this == PoseLandmarkType.rightHeel ||
this == PoseLandmarkType.rightFootIndex) {
return Colors.blue;
} else if (this == PoseLandmarkType.leftHip ||
this == PoseLandmarkType.leftKnee ||
this == PoseLandmarkType.leftAnkle ||
this == PoseLandmarkType.leftHeel ||
this == PoseLandmarkType.leftFootIndex) {
return Colors.pink;
} else if (this == PoseLandmarkType.leftShoulder ||
this == PoseLandmarkType.leftElbow ||
this == PoseLandmarkType.leftWrist ||
this == PoseLandmarkType.leftPinky ||
this == PoseLandmarkType.leftIndex ||
this == PoseLandmarkType.leftThumb) {
return Colors.deepPurple;
} else if (this == PoseLandmarkType.rightShoulder ||
this == PoseLandmarkType.rightElbow ||
this == PoseLandmarkType.rightWrist ||
this == PoseLandmarkType.rightPinky ||
this == PoseLandmarkType.rightIndex ||
this == PoseLandmarkType.rightThumb) {
return Colors.green;
} else {
return Colors.amber;
}
}
}
7. utilities.dart
について
-
getLocalPath()
- 端末のキャッシュを保存するディレクトリを取得。
- 処理の途中で生成されるファイルなどを覗くために、パスを
print()
しています。
-
getVideoMetadata()
- ビデオのピクセルサイズとフレームレートを取得。
- iPhoneでは縦撮影した内部的には横動画として記録した上でメタ情報の
orientation
で回転を与えて、実質的に縦動画として扱えるようにしています。そのため、元動画の見かけ通りのピクセルサイズ (width
よりheight
の方が大きい) を返すためにorientation
によって戻り値を分岐させています。
-
removeFFmpegFiles()
- ビデオを保存が完了してから、過程で生成されたキャッシュファイルを削除。
-
CommonValue
- アプリ内で使用する固定値をまとめるためのクラス。
ファイル名の接頭辞に使う文字列ffmpeg_
のみ。
- アプリ内で使用する固定値をまとめるためのクラス。
import 'dart:io';
import 'package:flutter_video_info/flutter_video_info.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
// アプリからファイルを保存するディレクトリのパス
Future<String> getLocalPath() async {
Directory tmpDocDir = await getTemporaryDirectory();
// ignore: avoid_print
print(tmpDocDir.path);
return tmpDocDir.path;
}
// ビデオのメタデータ取得
Future<Map<String, dynamic>?> getVideoMetadata(String videoFilePath) async {
final videoInfo = await FlutterVideoInfo().getVideoInfo(videoFilePath) as VideoData;
// 縦持ち撮影ファイル対応
if ((videoInfo.orientation! ~/ 90) % 2 == 1) {
return {
'width': videoInfo.height,
'height': videoInfo.width,
'fps': videoInfo.framerate,
};
} else {
return {
'width': videoInfo.width,
'height': videoInfo.height,
'fps': videoInfo.framerate,
};
}
}
// 生成される画像や動画のファイルのキャッシュを削除する
Future<void> removeFFmpegFiles() async {
// キャッシュディレクトリ取得
final localDirectory = await getTemporaryDirectory();
// ディレクトリ下の全てのファイル中で
for (var entry in localDirectory.listSync(recursive: true, followLinks: false)) {
// 本アプリで生成されたファイルを削除
final fileName = entry.path.split('/').last;
if (fileName.startsWith(CommonValue.filePrefix)) {
entry.deleteSync();
}
}
}
// アプリを通じて使うの固定の値
class CommonValue {
// 生成されるキャッシュファイルの名前の頭につける文字列
static String filePrefix = 'ffmpeg_';
}
これで全ファイルの解説は以上です!
おわりに
今回はシミュレーターでもすぐにお試しで出力が得られるサンプルとして動画でポーズ推定を行なってみました。
実用を考えると、Camera プラグインを利用してカメラから取得した映像をリアルタイムで扱う方が用途は広いと思います。
参考にしていただけましたらぜひ「いいね」をお願いいたします!
ご質問やご指摘、その他コメントいつでもお待ちしております。