【Flutter】Google's ML Kitを使い姿勢推定をした際に、検出位置が画像外の位置になってしまう
やりたかった事
Flutterで姿勢推定を行ない、入力した画像内の人の体の部位にマーカーを表示させようとしました。
事象
google_ml_kitを使い、画像内の人の姿勢推定をしようとした際に姿勢推定自体はエラー等が起こる事なく 体の部位の位置が検出することはできるが、出力された位置が表示画像のサイズを大きく超えており 想定していた位置にマーカーを配置することができなくなっていました。
実装
google_ml_kitを使っての姿勢推定を試してみたかっただけなので、基本的にはFlutterのボイラープレートそのままになっています。
この時点での画面表示
最初の画面です。
「画像を選択」ボタンを押下してローカルファイル内の画像を選択します
画像は表示されますが、肝心の姿勢推定の結果であるマーカーが表示されていません。
姿勢推定の結果、出力された数値
flutter: 右手の位置: 350.7270812988281, 972.6107788085938
flutter: 右肘の位置: 385.443603515625, 662.0947265625
flutter: 右肩の位置: 441.0372619628906, 413.0234375
姿勢推定自体は行なわれているようで、数値は出力されました。
表に起こしてみても、腕っぽい並びにはなっているので、デタラメな値でも無さそうです。
解決
原因は画像のサイズ縮小にありました。
入力した画像を画面表示させた際に、画像そのままのサイズではなく 画面幅に合わせて表示させるようにしていたために 表示されている画像のサイズと実際の画像ファイル情報を元に推定した数値が食い違っていたのが原因でした。
なので、表示させた画像の上に 表示されている人のポーズに合わせてマーカーを配置させたい場合は表示させている画像の縮小比率に合わせて推定結果を修正する必要があります。
画像が表示されるにあたって、2分の1のサイズに縮小されていた場合は マーカーを表示する数値も2/1するようにしてみました。
import 'dart:ffi';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_ml_kit/google_ml_kit.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';
class RenderLocalFile extends StatefulWidget {
const RenderLocalFile({Key? key}) : super(key: key);
_RenderLocalFileState createState() => _RenderLocalFileState();
}
class _RenderLocalFileState extends State<RenderLocalFile> {
final ImagePicker _picker = ImagePicker();
final PoseDetector _poseDetector =
PoseDetector(options: PoseDetectorOptions());
// 各部位の位置を保持する
Offset _rightHandOffset = Offset(0, 0);
Offset _rightElbowOffset = Offset(0, 0);
Offset _rightShoulderOffset = Offset(0, 0);
ui.Size _imageSize = ui.Size(0, 0);
File? _file;
// 画像を選択する
Future<void> _pickImage() async {
final XFile? file = await _picker.pickImage(source: ImageSource.gallery);
if (file == null) return;
setState(() {
_file = File(file.path);
});
_onImageSelected(file);
+ _getImageSize(file);
}
// 画像を選択した際の処理
Future<void> _onImageSelected(XFile image) async {
final Future<List<Pose>> _pose =
_poseDetector.processImage(InputImage.fromFilePath(image.path));
final completePose = await _pose;
if (completePose.isEmpty) {
print('姿勢が検出されませんでした');
return;
}
final PoseLandmark? rightHand =
completePose.first.landmarks[PoseLandmarkType.rightIndex];
final PoseLandmark? rightElbow =
completePose.first.landmarks[PoseLandmarkType.rightElbow];
final PoseLandmark? rightShoulder =
completePose.first.landmarks[PoseLandmarkType.rightShoulder];
setState(() {
_rightHandOffset = Offset(rightHand!.x, rightHand.y);
_rightElbowOffset = Offset(rightElbow!.x, rightElbow.y);
_rightShoulderOffset = Offset(rightShoulder!.x, rightShoulder.y);
});
}
+ // 画像のサイズを取得する
+ Future<void> _getImageSize(XFile file) async {
+ final data = await file.readAsBytes();
+ final codec = await ui.instantiateImageCodec(data);
+ final frameInfo = await codec.getNextFrame();
+ final image = frameInfo.image;
+ setState(() {
+ _imageSize = ui.Size(image.width.toDouble(), image.height.toDouble());
+ });
+ }
+ /// 画像のサイズと画像上のマーカーの位置から、画面上のマーカーの位置を計算する
+ /// @param screenHeight 画面の高さ
+ /// @param screenWidth 画面の幅
+ /// @param imageHeight 画像の高さ
+ /// @param imageWidth 画像の幅
+ /// @param markerOffset 画像上のマーカーの位置
+ /// @return 画面上のマーカーの位置
+ Offset _calculateMarkerPositionFromScreenSizeAndImageSizeAndMarkerOffset(
+ double screenHeight,
+ double screenWidth,
+ double imageHeight,
+ double imageWidth,
+ Offset markerOffset) {
+ final double scaledDx = (screenWidth / imageWidth) * markerOffset.dy;
+ final double scaledDy = (screenWidth / imageWidth) * markerOffset.dx;
+ return Offset(scaledDy, scaledDx);
+ }
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
_file == null
? Text('画像が選択されていません。')
+ : LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints constraints) {
+ final double screenWidth = constraints.maxWidth;
+ final double screenHeight = constraints.maxHeight;
+ final double imageWidth = _imageSize.width;
+ final double imageHeight = _imageSize.height;
+ final Offset rightHandPosition =
+ _calculateMarkerPositionFromScreenSizeAndImageSizeAndMarkerOffset(
+ screenHeight,
+ screenWidth,
+ imageHeight,
+ imageWidth,
+ _rightHandOffset);
+ final Offset rightElbowPosition =
+ _calculateMarkerPositionFromScreenSizeAndImageSizeAndMarkerOffset(
+ screenHeight,
+ screenWidth,
+ imageHeight,
+ imageWidth,
+ _rightElbowOffset);
+ final Offset rightShoulderPosition =
+ _calculateMarkerPositionFromScreenSizeAndImageSizeAndMarkerOffset(
+ screenHeight,
+ screenWidth,
+ imageHeight,
+ imageWidth,
+ _rightShoulderOffset);
return Stack(
children: <Widget>[
Image.file(_file!),
if (_rightHandOffset != null)
Positioned(
+ left: rightHandPosition.dx,
+ top: rightHandPosition.dy,
child: Container(
width: 20, // 円の直径
height: 20,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle, // 丸い形状
),
),
),
Positioned(
+ left: rightElbowPosition.dx,
+ top: rightElbowPosition.dy,
child: Container(
width: 20, // 円の直径
height: 20,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle, // 丸い形状
),
),
),
Positioned(
+ left: rightShoulderPosition.dx,
+ top: rightShoulderPosition.dy,
child: Container(
width: 20, // 円の直径
height: 20,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle, // 丸い形状
),
),
),
],
);
},
),
Positioned(
bottom: 20,
left: 20,
child: ElevatedButton(
onPressed: _pickImage,
child: Text('画像を選択'),
),
),
]),
));
}
}
画面表示
無事右肩・右肘・右手にマーカーを表示することができました。
今回は、画像が表示されるにあたって縮小率等を計算する際に 画像が縦長である事を全体とした実装にしていましたが、実際に動かしていく場合は横長にも対応させていく必要がありそうです。
が、google_ml_kitの挙動確認というところでは十分達成できたと思うので、以上で終わります。
株式会社SKIYAKIのテックブログです。ファンクラブプラットフォームBitfanの開発・運用にまつわる知見や調べたことなどを発信します。主な技術スタックは Ruby on Rails / React / AWS / Swift / Kotlin などです。 recruit.skiyaki.com/
Discussion