Develop/Flutter

[Flutter] 플러터 tflite(TensorFlow Lite)를 활용한 이미지 분류

JunJangE 2022. 1. 18. 16:11

이전에 image_picker 패키지를 통해 카메라를 구현해보았다.

 

[Flutter] 플러터 image_picker 패키지를 통해 카메라 구현

이번에는 Flutter에서 카메라를 구현해보려고 했다. 카메라 구현을 위해 여러 패키지를 알아보았지만 UI 커스터마이징은 불가능한 것으로 보였고 다른 분들에 오픈 소스 코드도 확인해보았지만

fre2-dom.tistory.com

이번에는 이전에 만든 image_picker 패키지를 가지고 tflite를 활용하여 이미지 분류를 하는 앱을 만들어보려고 한다.

tflite(TensorFlow Lite)

tflite를 알기 위해서는 우선 TensorFlow부터 알아야 한다.

TensorFlow는 인간이 사용하는 학습 및 추론과 유사한 패턴과 상관관계를 감지하고 해독할 수 있는 신경망을 구축하고 학습하기 위한 플랫폼이다.
TensorFlow의 유연한 아키텍처를 통해 개발자는 단일 API를 사용하여 데스크톱, 서버 또는 모바일 장치에서 하나 이상의 CPU 또는 GPU에 계산을 배포할 수 있다. 원래는 기계 학습 및 심층 신경망 연구를 수행하기 위해 기계 지능 연구 부서의 Google 두뇌 팀에서 일하는 연구원과 엔지니어가 개발했다. 

그렇다면 TensorFlow Lite는 무엇일까? TensorFlow Lite는 에지 기기에서 TensorFlow 모델 추론을 실행하기 위한 공식 프레임 워크이다. Android, iOS 및 Linux 기반 IoT 장치를 포함한 다양한 플랫폼과 베어 메탈 마이크로 컨트롤러에서 전 세계적으로 40 억 개 이상의 활성 장치에서 실행된다.

Lite 버전에 장점으로는 다음과 같다.

  • TensorFlow Lite는 작은 바이너리 크기와 빠른 초기화로 경량으로 설계되었다.
  • 또한 Android 및 iOS를 포함한 다양한 플랫폼과 호환된다.
  • 모바일 경험을 향상하기 위해 로드 시간과 하드웨어 가속이 개선된 모바일 장치에 최적화되어 있다.

라이브러리

flutter에서 Tensor Flow Lite를 활용하기 위해서는 pub.dev에 들어가 패키지를 다운로드하면 된다.

 

tflite | Flutter Package

A Flutter plugin for accessing TensorFlow Lite. Supports both iOS and Android.

pub.dev

새 프로젝트를 생성하고 위 링크에 들어가 Installing의 다음 패키지를 pubspec.yaml에 추가하여 다운로드한다.

dependencies:
  tflite: ^1.1.2

다음으로 android > app > build.gradle에 들어가 다음 코드를 android 블록에 추가한다.

   ...
   aaptOptions {
        noCompress 'tflite'
        noCompress 'lite'
    }
  ...

그리고 defaultConfig { } 안에 minSdkVersion을 21로 설정한다.

ios의 경우에는 Xcode에서 Flutter 프로젝트 > ios > PROJECT의 Runner를 선택하고 iOS Deployment Target을 9.0으로 변경하면 된다고 한다. (맥북이 없어서 따로 시도해보지는 못했다..)

모델

다음으로 애플리케이션에 assets 폴더를 만들어 txt 파일과 모델을 추가한다.

모델이 없는 경우 아래 깃허브에서 예시 모델을 사용하면 될 것 같다.

추가한 후에는 pubspec.yaml에 다음 코드를 작성하여 다운로드한다.

...
assets:
   - assets/
...

이제 본격적으로 코드를 작성해보자. 

이전에 impage_picker 코드와 중복되는 내용이 있으니 그 내용은 빼고 설명을 해놓았다.

main.dart

import 'package:flutter/material.dart';
import 'camera_ex.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
      
        primarySwatch: Colors.green,
      ),
      home: CameraExample(),
    );
  }
}

camera.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:tflite/tflite.dart';

class CameraExample extends StatefulWidget {
  const CameraExample({Key? key}) : super(key: key);

  @override
  _CameraExampleState createState() => _CameraExampleState();
}

class _CameraExampleState extends State<CameraExample> {
  File? _image;
  final picker = ImagePicker();
  List? _outputs;

  // 앱이 실행될 때 loadModel 호출
  @override
  void initState() {
    super.initState();
    loadModel().then((value) {
      setState(() {});
    });
  }

  // 모델과 label.txt를 가져온다.
  loadModel() async {
    await Tflite.loadModel(
      model: "assets/model_unquant.tflite",
      labels: "assets/testlabel.txt",
    ).then((value) {
      setState(() {
        //_loading = false;
      });
    });
  }

  // 비동기 처리를 통해 카메라와 갤러리에서 이미지를 가져온다.
  Future getImage(ImageSource imageSource) async {
    final image = await picker.pickImage(source: imageSource);

    setState(() {
      _image = File(image!.path); // 가져온 이미지를 _image에 저장
    });
    await classifyImage(File(image!.path)); // 가져온 이미지를 분류 하기 위해 await을 사용
  }

  // 이미지 분류
  Future classifyImage(File image) async {
    print("asdasddas$image");
    var output = await Tflite.runModelOnImage(
        path: image.path,
        imageMean: 0.0, // defaults to 117.0
        imageStd: 255.0, // defaults to 1.0
        numResults: 2, // defaults to 5
        threshold: 0.2, // defaults to 0.1
        asynch: true // defaults to true
        );
    setState(() {
      _outputs = output;
    });
  }

  // 이미지를 보여주는 위젯
  Widget showImage() {
    return Container(
        color: const Color(0xffd0cece),
        margin: EdgeInsets.only(left: 95, right: 95),
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.width,
        child: Center(
            child: _image == null
                ? Text('No image selected.')
                : Image.file(File(_image!.path))));
  }

  recycleDialog() {
    _outputs != null
        ? showDialog(
            context: context,
            barrierDismissible:
                false, // barrierDismissible - Dialog를 제외한 다른 화면 터치 x
            builder: (BuildContext context) {
              return AlertDialog(
                // RoundedRectangleBorder - Dialog 화면 모서리 둥글게 조절
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10.0)),
                content: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      _outputs![0]['label'].toString().toUpperCase(),
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 15.0,
                        background: Paint()..color = Colors.white,
                      ),
                    ),
                  ],
                ),
                actions: <Widget>[
                  Center(
                    child: new FlatButton(
                      child: new Text("Ok"),
                      onPressed: () {
                        Navigator.pop(context);
                      },
                    ),
                  )
                ],
              );
            })
        : showDialog(
            context: context,
            barrierDismissible: false,
            builder: (BuildContext context) {
              return AlertDialog(
                content: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      "데이터가 없거나 잘못된 이미지 입니다.",
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 15.0,
                      ),
                    ),
                  ],
                ),
                actions: <Widget>[
                  Center(
                    child: new FlatButton(
                      child: new Text("Ok"),
                      onPressed: () {
                        Navigator.pop(context);
                      },
                    ),
                  )
                ],
              );
            });
  }

  @override
  Widget build(BuildContext context) {
    // 화면 세로 고정
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

    return Scaffold(
        backgroundColor: const Color(0xfff4f3f9),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Classify',
              style: TextStyle(fontSize: 25, color: const Color(0xff1ea271)),
            ),
            SizedBox(height: 25.0),
            showImage(),
            SizedBox(
              height: 50.0,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                // 카메라 촬영 버튼
                FloatingActionButton(
                  child: Icon(Icons.add_a_photo),
                  tooltip: 'pick Iamge',
                  onPressed: () async {
                    await getImage(ImageSource.camera);
                    recycleDialog();
                  },
                ),

                // 갤러리에서 이미지를 가져오는 버튼
                FloatingActionButton(
                  child: Icon(Icons.wallpaper),
                  tooltip: 'pick Iamge',
                  onPressed: () async {
                    await getImage(ImageSource.gallery);
                    recycleDialog();
                  },
                ),
              ],
            )
          ],
        ));
  }

  // 앱이 종료될 때 
  @override
  void dispose() {
    Tflite.close();
    super.dispose();
  }
}

코드의 흐름을 확인해 보면 다음과 같다.

앱이 실행될 때 initState()를 통해 loadModel()을 호출하게 되고 호출되면서 모델과 label.txt를 가져오게 된다.

다음으로 showImage()인 이미지를 보여주는 위젯이 실행되며 이미지가 없으면 텍스트 위젯에서 "No image selected"를 UI에 뿌리게 되고 이미지가 있으면 그 이미지를 UI에 뿌려준다.

다음으로 2개의 FloatingActionButton을 통해 getImage() 위젯에서 이미지를 가져오게 되고 가져온 이미지를 setState()를 통해 위에 showImage() 위젯의 image의 상태를 변화시켜 UI에 뿌려주게 된다. 이제 그 이미지가 어떠한 이미지인지 분류하기 위해 classifyImage()를 await을 통해 실행한다.(await을 사용하지 않으면 새로 가져온 이미지를 분류하지 않고 이전에 있었던 이미지를 가지고 분류하는 경우가 생기게 된다.)

다음으로 classifyImage() 위젯을 수행하면서 어떠한 이미지인지 output이 나오게 되는데, 이걸 setState()를 통해 상태 변화를 시키고 recycleDialog()에 showDialog를 통해 어떤 이미지인지 output을 UI에 뿌려주게 된다.

마지막으로 앱이 종료될 때 Tfilte가 종료되게 된다.

위 코드를 모두 실행하면 다음 영상과 같이 실행되는 것을 확인할 수 있다.

참고

 

플러터(flutter) - 텐서플로우(tensorflow lite) 예제 소개

아래의 영상은 플러터(flutter)에서 텐서플로우(tensorflow)를 사용하는 예제를 소개하는 유튜브 영상이다....

blog.naver.com

 

tflite | Flutter Package

A Flutter plugin for accessing TensorFlow Lite. Supports both iOS and Android.

pub.dev

 

Tensorflow Lite 및 Flutter를 사용한 실시간 이미지 분류

스마트 폰과 같은 엣지 디바이스는 시간이 지남에 따라 더욱 강력 해졌으며 점점 더 많은 온 디바이스 머신 러닝 사용 사례를 가능하게했습니다. Tensorflow는 무엇입니까? TensorFlow는 인간이 사용

ichi.pro

 

Flutter 다이얼로그 위젯 정리

사용자의 확인을 요구하거나 팝업메시지 등을 표시해주고 싶을 때 사용하는 다이얼로그 위젯에 대해 정리해보자. 요약 : AlertDialog, DatePicker, TimePicker AlertDialog - title : 제목 영역 - content : 내용..

kyungsnim.net

gitbub

 

GitHub - junjange/Flutter-Learning: 플러터 학습

플러터 학습. Contribute to junjange/Flutter-Learning development by creating an account on GitHub.

github.com