Flutter 에서 Google ML Kit으로 아기사진 골라내기

지난번에 Google ML Kit for Flutter의 Face Detection을 이용하는 간단한 앱을 만들어 봤습니다.

오늘은 Image Labeling을 이용해서 아기사진만 골라내는 코드를 작성해 보았습니다. 지난번 코드에서 크게 달라지는 부분은 없습니다. FaceDetection 관련된 로직이 Image Labeling으로 바뀌는 정도 입니다.

자세한 정보는 아래 링크를 참고하세요.

주요 소스코드

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
위 광고를 클릭해주시면 코드가 나타납니다.
// .. 전략
// 간단하게 클래스 하나 정의하구요
class ImageObj {
  final String title;
  final Image image;
  ImageObj({
    required this.title,
    required this.image,
  });
}

class _MyHomePageState extends State<MyHomePage> {
// 스테이트 클래스 내부에 아래처럼 위에서 정의한 클래스와 
// 이미지 라벨링 용 오브젝트를 선언합니다.
  List<ImageObj> images = [];
  final imageLabeler =
        ImageLabeler(options: ImageLabelerOptions(confidenceThreshold: 0.5));
  
  Future<void> _doDetect() async {
    final ImagePicker picker = ImagePicker();
    // 이미지 선택 다이얼로그 띄웁니다
    List<XFile> imagesFromPicker = await picker.pickMultiImage();
    List<bool> hasBabyIndex = [];
    List<String> textLabels = [];
    List<double> confidences = [];
    images = [];
    for(XFile item in imagesFromPicker) {
      // 이미지 피커에서 사용하는 XFile 타입을 ML Kit에서 사용하는 InputImage 형태로 변경
        final List<ImageLabel> labels =
                await imageLabeler.processImage(InputImage.fromFilePath(item.path));
        bool hasBaby = false;
        for (ImageLabel label in labels) {
          // 아기 사진 레이블의 색인(index)이 421번 입니다.
          if (label.index == 421) {
            hasBaby = true;
          }
        }
        hasBabyIndex.add(hasBaby);
    }
    setState(() {
      int i = 0;
      for(XFile item in imagesFromPicker) {
        // XFile 타입을 이미지 뷰어에서 사용하는 Image 타입으로 변환하기 위해서
        // cross_file_image 패키지를 여기서 사용합니다.
        images.add(ImageObj(
            title: hasBabyIndex[i++] ? "Baby" : "No Baby",
            image: Image(image: XFileImage(item))));
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: images.length > 0 ? ListView.builder(
            itemCount: images.length,
            itemBuilder: (context, index) {
                  return ListTile(
                    leading: images[index].image,
                    title: Text(images[index].title),
                    onTap: () {
                    },
                );
        }) : Text("No Images"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _doDetect,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

수행결과

  • 이미지1
  • 그 전에 No Human으로 나왔던 사진과 사람이 아닌 사진을 따로 뽑아서 돌려봤습니다.
  • 보시다시피 이제는 Baby로 체크해주네요. 정확성이 얼굴인식보다는 더 높아 보입니다.
  • 다만 엄마와 함께 찍은 사진은 다른 레이블로 분류되고 있습니다.
    • 이럴때는 커스텀 모델을 만들거나 객체 감지 기술을 이용해야 할 것으로 보입니다.
  • 다음 시간에는 객체 감지 기술에 대해 알아보도록 하겠습니다.

Flutter 에서 Google ML Kit으로 얼굴인식 수행하기

오늘은 Flutter에서 다양한 유형의 사진을 다중선택 한 후 Google ML Kit의 얼굴인식 기능을 이용하여 사람이 사진 속에 존재하는지를 판정하는 간단한 앱을 만들어 보겠습니다.

사진 다중 선택은 예전에도 사용해봤던 아래 패키지를 이용하고자 합니다.

아울러서 이미지 타입을 용이하게 변환하고자 다음 패키지도 이용합니다.

주요 소스코드

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
위 광고를 클릭해주시면 코드가 나타납니다.
// .. 전략
// 간단하게 클래스 하나 정의하구요
class ImageObj {
  final String title;
  final Image image;
  ImageObj({
    required this.title,
    required this.image,
  });
}

class _MyHomePageState extends State<MyHomePage> {
// 스테이트 클래스 내부에 아래처럼 위에서 정의한 클래스와 
// 얼굴인식 용 오브젝트를 선언합니다.
  List<ImageObj> images = [];
  final FaceDetector _faceDetector = FaceDetector(
    options: FaceDetectorOptions(
      enableContours: true,
      enableClassification: true,
    ),
  );
  Future<void> _doDetect() async {
    final ImagePicker picker = ImagePicker();
    // 이미지 선택 다이얼로그 띄웁니다
    List<XFile> imagesFromPicker = await picker.pickMultiImage();
    List<int> faceCount = [];
    images = [];
    for(XFile item in imagesFromPicker) {
      // 이미지 피커에서 사용하는 XFile 타입을 ML Kit에서 사용하는 InputImage 형태로 변경
      final faces = await _faceDetector.processImage(InputImage.fromFilePath(item.path));
      faceCount.add(faces.length);
    }
    setState(() {
      int i = 0;
      for(XFile item in imagesFromPicker) {
        // XFile 타입을 이미지 뷰어에서 사용하는 Image 타입으로 변환하기 위해서
        // cross_file_image 패키지를 여기서 사용합니다.
        images.add(
          ImageObj(title: faceCount[i++] > 0 ? 
            "Human" : "No Human", image: Image(image: XFileImage(item))));
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: images.length > 0 ? ListView.builder(
            itemCount: images.length,
            itemBuilder: (context, index) {
                  return ListTile(
                    leading: images[index].image,
                    title: Text(images[index].title),
                    onTap: () {
                    },
                );
        }) : Text("No Images"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _doDetect,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

수행결과

  • 이미지1
  • 이미지2
  • 사람이 아닌 것은 확실히 “아니라고” 합니다. 정확히 표현하면 사람 얼굴을 0개 찾아냅니다.
    • 빵, 꽃, 고양이 사진을 선택했는데 모두 아니라고 나옵니다.
  • 그런데 사람의 경우에는 정면사진이 아닐 경우 오인식이 나타나는군요.
    • 아무래도 ML 모델의 원리가 눈도 2쌍, 귀도 2쌍이어야 하고
    • 동공이 측정되어야하고 이목구비의 거리 및 비율 등을 특징값으로 학습한 것이 아닐까 추측됩니다.
    • 옆 모습이나, 눈감은 모습, 누워있는 모습은 얼굴이 0개로 측정되는 경우가 있습니다.

Flutter 에서 flutter_blue_plus 이용시 릴리즈모드 에러 해결하기

문제의 상황

  • Flutter를 이용해서 bluetooth 연결을 하고자 합니다.
  • 그래서 저는 flutter_blue_plus라는 패키지를 이용하기로 했습니다.
  • 디버그 모드에서는 잘 돌아갔으나, 릴리즈 모드에서 아래와 같은 에러가 발생하면서 기기 스캔이 정상동작 하지 않더군요.
      E/flutter (20889): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] 
      Unhandled Exception: PlatformException(startScan, 
      Field androidScanMode_ for j.e0 not found. 
      Known fields are [private int j.e0.h, 
      private k.b0$i j.e0.i, private boolean j.e0.j, private static final j.e0 
      j.e0.k, private static volatile k.a1 j.e0.l], 
      java.lang.RuntimeException: Field androidScanMode_ for j.e0 not found. 
      Known fields are [private int j.e0.h, private k.b0$i j.e0.i, 
      private boolean j.e0.j, private static final j.e0 j.e0.k, private static volatile k.a1 j.e0.l]
    
      E/flutter (20889):      at k.v0.n0(Unknown Source:72)
      E/flutter (20889):      at k.v0.T(Unknown Source:655)
      E/flutter (20889):      at k.v0.R(Unknown Source:12)
      E/flutter (20889):      at k.k0.e(Unknown Source:60)
      E/flutter (20889):      at k.k0.a(Unknown Source:49)
      E/flutter (20889):      at k.d1.d(Unknown Source:17)
      E/flutter (20889):      at k.d1.e(Unknown Source:4)
      E/flutter (20889):      at k.z$a.z(Unknown Source:9)
      E/flutter (20889):      at k.z$a.y(Unknown Source:4)
    ... 생략
    

원인은?

  • 원인을 파악해보니 android/app/build.gradle 파일에서 릴리즈 모드시에 설정한 코드 축소관련 내용 때문이었습니다.
      shrinkResources true
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'       
    
  • minifyEnabled 는 코드를 축소해서 릴리즈시켜주고, shrinkResources 는 리소스를 축소키져주죠. 여기서 충돌이 일어났던것 같군요.
  • 둘다 false로 해주면 정상동작이야 하지만, 이러면 앱이 무거워지므로 좋은 방법이 아닙니다.

해결책

  • proguard-rules에서 해당 의존성을 제외해주세요. proguard-rules.pro 파일에 보면 아래와 같이 축소대상에서 제외되는 패키지들을 지정할 수 있는데요.
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }

-keep class com.boskokg.flutter_blue_plus.** { *; }

-dontwarn io.flutter.embedding.**
  • flutter_blue_plus 는 위와 같이 지정해주면 됩니다. github 주소를 보면 전체주소 값이 추측이 됩니다.
    • https://github.com/boskokg/flutter_blue_plus