📖 YOLO
- Flutter 애플리케이션에서
YOLO (You Only Look Once) 객체 탐지 모델을 통합하여 사용하는 것을 의미한다.
- 즉 Flutter 앱에서 실시간 이미지나 비디오 속 객체를 빠르고 정확하게 탐지할 수 있게 만드는 기술이다.
⚙️ YOLO 개요
- 단일 컨볼루션 신경망 (
CNN)을 사용해 이미지나 비디오 내 객체를 한 번에 감지하는 컴퓨터 비전 모델이다.
- 실시간으로 여러 객체를 동시에 빠르고 정확하게 탐지 가능하다.
- 객체 탐지를 회귀 문제로 접근하여 한 번의 네트워크 통과로 클래스와 위치를 동시에 예측한다.
🧩 YOLO 통합 방법
모델 변환
- 학습된
YOLO 모델을 TensorFlow Lite 형식으로 변환한다.
TFLite는 경량화된 머신러닝 프레임워크로 모바일 환경에서 효율적으로 작동한다.
모델 파일 포함
- 변환된
.tflite 모델 파일을 Flutter 프로젝트의 assets 폴더에 추가한다.
실시간 이미지 입력
camera 패키지를 이용해 디바이스 카메라 스트림을 받아온다.
- 프레임 단위로 이미지를 캡처하여 모델에 입력한다.
추론 처리
tflite_flutter 패키지를 활용해 TFLite 모델을 로드 및 실행한다.
- 결과로 바운딩 박스 + 라벨 + 확률 값을 획득한다.
UI 시각화
CustomPaint 또는 Canvas 위젯을 사용해 탐지된 객체의 박스와 이름을 화면에 오버레이 한다.
🧵 성능 최적화 포인트
비동기 처리
- 카메라 프레임 전처리 및 모델 추론은 백그라운드
Isolate 또는 네이티브 코드에서 실행한다.
- 이를 통해 UI 프레임 멈춤을 방지한다.
모델 최적화
- 양자화하여 모델 크기를 낮추고 속도를 높인다.
GPU / NNAPI / Metal을 가속에 사용한다.
- 입력 해상도를 적절히 조정하여 FPS를 확보한다.
경량 모델 사용
- 모바일 환경에서는
YOLOv8-nano, YOLOv5s 등 경량화 모델을 사용해 정확도와 속도의 균형을 유지한다.
💡 활용 사례
| 분야 | 설명 |
|---|
| 보안 감시 시스템 | 카메라 영상을 실시간 분석하여 침입자 감지 |
| 자율 주행 보조 앱 | 도로 위 차량, 보행자 인식 |
| 소매 재고 관리 | 상품 인식 및 자동 재고 체크 |
| 의료 영상 분석 | 영상 내 이상 부위 탐지 |
| AR 기반 애플리케이션 | 현실 객체 인식 후 가상요소 오버레이 |
🧠 YOLO 객체 감지 바운딩 박스 코드
class Bbox extends StatelessWidget {
Bbox({
required this.detectedObject,
required this.imageWidth,
required this.imageHeight,
required this.label,
});
final DetectedObject detectedObject;
final int imageWidth;
final int imageHeight;
final String label;
@override
Widget build(BuildContext context) {
final deviceWidth = MediaQuery.sizeOf(context).width;
final resizeFactor = deviceWidth / imageWidth;
final resizedX = detectedObject.x * resizeFactor;
final resizedY = detectedObject.y * resizeFactor;
final resizedW = detectedObject.width * resizeFactor;
final resizedH = detectedObject.height * resizeFactor;
final random = Random();
return Positioned(
left: resizedX - resizedW / 2,
top: resizedY - resizedH / 2,
child: Container(
width: resizedW,
height: resizedH,
decoration: BoxDecoration(
border: Border.all(
color: Color(
0xFF000000 + random.nextInt(0xFFFFFF),
),
width: 3)),
child: Text(
label,
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
);
}
}
🧠 YOLO 이미지 탐지 코드
class YoloDetection {
List<String>? _labels;
String label(int index) => _labels![index];
Interpreter? _interpreter;
bool get isInit => _interpreter != null && _labels != null;
Future<void> init() async {
_interpreter = await Interpreter.fromAsset('assets/yolov8.tflite');
final labelStrings = await rootBundle.loadString('assets/labels.txt');
_labels = labelStrings.split('\n');
}
List<DetectedObject> runInference(Image image) {
if (!isInit) {
throw Exception('The model must be initialized');
}
final resizedImage = copyResize(image, width: 640, height: 640);
final imageNormalized = List.generate(
640,
(y) => List.generate(
640,
(x) {
final pixel = resizedImage.getPixel(x, y);
return [pixel.rNormalized, pixel.gNormalized, pixel.bNormalized];
},
),
);
final output = [
List<List<double>>.filled(
84,
List<double>.filled(8400, 0),
)
];
_interpreter!.run([imageNormalized], output);
return YoloHelper.parse(
output[0],
image.width,
image.height,
);
}
}
🧠 YOLO 적용
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final model = YoloDetection();
final picker = ImagePicker();
List<DetectedObject>? detectedObjects;
Uint8List? imageBytes;
int? imageWidth;
int? imageHeight;
@override
void initState() {
super.initState();
model.init();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: GestureDetector(
onTap: () async {
final xFile = await picker.pickImage(source: ImageSource.gallery);
if (xFile != null) {
final bytes = await xFile.readAsBytes();
final image = img.decodeImage(bytes);
final results = model.runInference(image!);
imageHeight = image.height;
imageWidth = image.width;
setState(() {
detectedObjects = results;
imageBytes = bytes;
});
}
},
child: ListView(
children: [
if (imageBytes == null)
Icon(
Icons.file_open_outlined,
size: 80,
)
else
Stack(
children: [
AspectRatio(
aspectRatio: imageWidth! / imageHeight!,
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
)),
if (detectedObjects != null)
...detectedObjects!.map((e) {
return Bbox(
detectedObject: e,
imageWidth: imageWidth!,
imageHeight: imageHeight!,
label: model.label(e.labelIndex),
);
})
],
),
],
),
),
);
}
}