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

GCP와 GKE 기술용어 정리

PUB/SUB

  • GPC용 카프카(Kafka)라고 보면 됩니다. MS(MicroServices)기반의 서비스 플랫폼 환경에서 운영메시지 및 데이터를 활용하기 위해서는 Kafka와 같은 구독기반 시스템이 필요할 때가 있습니다.
  • 제가 구축했던 서비스에서 사용된 데이터는 2가지 특징이 있었는데요.
    • 첫째 대용량 데이터가 있었어요. AI 모델링을 위한 학습데이터 였죠. 양이 많을 수 밖에 없습니다.
    • 둘째 다방면에서 사용되었습니다. 음성데이터를 텍스트 데이터로 그대로 옮긴 후에 단어에 특정 라벨링을 해서 학습서버에 보내기도하지만, 그 라벨링이 얼마나 사용되었는지 카운팅해주는 통계서버에도 데이터를 보내야 하는 경우가 있었죠.

Cloud Speech-to-text

  • 음성을 텍스트로 변환해주는 서비스 입니다.
  • 취급하는 음성데이터의 단위가 짧은(15초단위로 과금) 방식과, 긴 파일을 한꺼번에 변환해주는 방식이 있습니다.
  • 부가기능중에 화자분리가 있는데, 한국어는 지원을 안해주는 문제가 있었어요.
  • 제가 취급했던 음성 데이터는 채널이 분리되서 녹음되는 케이스라 굳이 화자분리를 할 필요는 없었지만, 긴 파일을 한꺼번에 변환해주는 방식보다는 짧은 파일을 변환해주는 방식이 비용이 덜 나가더라구요. 그래서 채널별로 15초 단위로 잘라서 파일을 보내는 루틴을 개발해야 했습니다.

Dialogflow

  • 대화형 인터페이스 서비스 입니다. 챗봇 같은거라고 생각하면 편하지만, 조금더 확장성이 있습니다.
  • 어떻게 보면 챗봇 + 설치형 웹서비스라고 보는게 맞을수도 있겠네요.
  • 이 서비스를 이용해서 모호한 표현이나 잘못된 표현이 들어간 주소를 말해도 정확한 우편번호를 찾아주는 서비스를 만들어 보려고 했는데 생각보다 쉽지 않았던 기억이 있습니다.
    • 예를들어 “서울 중구 세종대로 110”으로 말해야 할 것을 “서울”을 생략하고 말한다거나, “세종대로”를 “세종로”로 한다거나 했을 때도 정확한 위치를 파악해서 우편번호를 찾아주는 서비스 말이죠.
    • 말로 표현할 때는 저렇게 생략되거나 중간중간 잘못된 표현을 사용할 확률이 높기 때문이죠, 그런데 구현이 쉽지 않더라구요.

Cloud DNS

  • Compute Engine이나 Kubernetes Engine에 설치된 가상머신이나 서비스IP를 연결할 수 있습니다. 일반적인 웹호스팅 업체에서 제공하는 기능에 추가적으로 G메일이나 G-Suite같은 기능도 연결해서 사용할 수 있습니다.
  • HTTPS를 이용할 때 인증서 문제가 있는데요, 구글에서 관리해주는 방식이 있고 직접 만든 인증서를 사용하는 방식이 있습니다. 저 같은 경우엔 Lets Encrypts를 이용해서 3개월에 한번씩 갱신해주는 인증서도 써봤고, 돈 주고 산 인증서도 써봤는데 이럴때 후자의 방식을 사용합니다.

Cloud Storage

  • AWS의 S3에 대응되는 서비스라고 보시면 됩니다.
  • 써드파티 사용자권한에 따라서 접근권한을 달리하는 서비스를 구상중이었는데, 퇴사하면서 개발하지 못한 경험이 있네요.

Persistent Disk

  • Compute Engine이나 Kubernetes Engine에서 사용하는 가상하드디스크라고 생각하면 됩니다.
  • SSD를 이용하는게 있고 일반 HDD를 이용하는게 있는데 당연히 SSD 이용하면 비싸겠죠.
  • 백업과 관련하여 신경쓸 부분들이 몇가지 있습니다. 수백기가 정도의 데이터도 상당히 빠르게 백업합니다만, 그 순간에 살짝 느려지는 현상이 있더군요. 그래서 백업을 주로 서비스타임이 아닌 새벽시간에 돌아가도록 해놓았지만, 서비스 업그레이드를 하거나 뭔가 이슈가 발생할 수 있는 상황에서 민감하게 백업을 할 수 있어야 할 것 입니다. 복원하는 과정도 마찬가지죠. 이럴때 분초를 다투는 사투가 벌어질 수 있기 때문에, 미리미리 연습해두는 것도 좋습니다.

Cloud IAM

  • 계정별로 각종 GCP 서비스에 어떻게 접근하게 할 것인지 제어합니다.
  • 예를들어 Cloud Storage에서는 업로드 권한을 주지만 Compute Engine Console에는 접근을 못하게 한다던가 하는 행위를 정의할 수 있는거죠.

Compute Engine

  • 가상서버라고 생각하면 됩니다. 사용하는 환경(MEMORY, CPU, GPU, TPU)에 따라서 가격이 천차만별입니다. 약정하면 더 저렴하게 쓸 수 있구요.

Kubernetes Engeine

  • 위에서 소개한 가상서버 여러개를 묶어서 클러스터를 만듭니다. 그리고 그것들의 자원을 논리적으로 분할하여 여러개의 MS를 넣다/뺐다 하면서 사용합니다.
  • 이 서비스를 사용하기 위해서는 다양한 세부기능과 이슈들을 경험해보는 것이 좋습니다. 그래야 실전에서 효과적으로 사용할 수 있기 때문이죠. 키워드로 정리해보면 다음과 같습니다.
    • Pod를 구성하는 방법 : 스케일 아웃 전략 정의하기, 서비스 정상구동확인 및 재부팅 방식 정의하기, 가상화 이미지 연동시키기(like Docker Image)
    • Persistent Disk를 연결하기
    • 외부와의 연결 : Pod 하나에 사설 IP를 바로 연결하는 방법도 있고, ENVOY나 NGINX등을 이용해서 로드밸런싱 하는 방법도 있습니다.

Cloud Function

  • 서버리스 컴퓨팅 입니다. GCP의 람다 같은거라고 생각하면 됩니다.
  • 제가 사용해본 케이스는 아래와 같습니다.
    • VueJS로 만든 웹프런트 저작도구가 있었습니다. 여기서 데이터가 만들어지면 Go언어 기번의 백엔드 서버로 넘어가는데요, 프런트에서 발생한 오류를 수집하고자 할 때 Cloud Function을 이용했습니다.
    • 처음에는 자체적으로 Fluentd 백엔드 서버를 구성해서 거기로 로그를 수집하려고 했지만, 시간을 아껴야만 했거든요.
    • 그래서 Cloud Function에서 데이터를 받아서 바로 Cloud Logging 서비스로 저장하는 간단한 파이썬 코드를 짜서 로그를 수집했었습니다.