Flutter 에서 yolov8 사용하기 - 1
Flutter 에서 yolov8 을 사용하려면 이 방법이 가장 주효하다.
물론 더 나은 라이브러리가 제공된다면 좋을 것이다.
tflite_flutter 라이브러리가 현재 공식 지원이다.
예제 등을 찾아보면 없을 것이다.
어찌하여 이미지를 추론하는 것은 찾을 수 있다.
문제는 동영상을 추론하는 게 필요하다.
그에 대한 예제는 ssd_mobilenet 모델을 제공하고 있다.
ultralytics 에서 제공하는 export 방법을 사용하여 모바일용 모델을 생성한다.
format='tflite' 옵션을 사용하면 3가지 속성이 default 로 설정된다.
half=False, int8=False, imgsz=640 이다.
각각의 설정을 변경하면서 테스트 하여 속도를 체감해보길 권장한다.
나의 경우는 half 와 int8 은 양자화인데 속도에 크게 좌우되지 않고 정밀도를 낮추는거 같았다.
imgsz 를 조정하였는데 224, 320, 4800, 640 순으로 진행하였는데
224 가 속도가 가장 빨랐다.
export 하는 중에 input 텐서의 속성을 알려주는데 이게 중요하다.
[1, 224, 224, 3] 으로 나올 것이다.
output 텐서는 [1, 6, 1029] 이런 식으로 나올 것이다.
이 텐서의 3 ~ 4차원 행렬을 무시해서 삽질을 오래 하였다.
그리고 이런 부분에 대한 설명이 자세하지 않아서
인공지능 및 딥러닝을 전공하지 않은 개발자들에게 허들로 작용할 수 있다.
모델은 여기서 가져와서 이렇게 사용한다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum _Codes { init, busy, ready, detect, result } | |
class _Command { | |
const _Command(this.code, {this.args}); | |
final _Codes code; | |
final List<Object>? args; | |
} | |
class Detector { | |
static const String _modelPath = 'assets/models/elf.tflite'; | |
static const String _labelPath = 'assets/models/elf.txt'; | |
Detector._(this._isolate, this._interpreter, this._labels); | |
final Isolate _isolate; | |
late final Interpreter _interpreter; | |
late final List<String> _labels; | |
late final SendPort _sendPort; | |
bool _isReady = false; | |
final StreamController<Map<String, dynamic>> resultsStream = | |
StreamController<Map<String, dynamic>>(); | |
static Future<Detector> start() async { | |
final ReceivePort receivePort = ReceivePort(); | |
final Isolate isolate = | |
await Isolate.spawn(_DetectorServer._run, receivePort.sendPort); | |
final Detector result = Detector._( | |
isolate, | |
await _loadModel(), | |
await _loadLabels(), | |
); | |
receivePort.listen((message) { | |
result._handleCommand(message as _Command); | |
}); | |
return result; | |
} | |
static Future<Interpreter> _loadModel() async { | |
final interpreterOptions = InterpreterOptions(); | |
// Use XNNPACK Delegate | |
if (Platform.isAndroid) { | |
interpreterOptions.addDelegate(XNNPackDelegate()); | |
} | |
return Interpreter.fromAsset( | |
_modelPath, | |
options: interpreterOptions..threads = 4, | |
); | |
} | |
static Future<List<String>> _loadLabels() async { | |
return (await rootBundle.loadString(_labelPath)).split('\n'); | |
} | |
void processFrame(CameraImage cameraImage) { | |
if (_isReady) { | |
_sendPort.send(_Command(_Codes.detect, args: [cameraImage])); | |
} | |
} | |
void _handleCommand(_Command command) { | |
switch (command.code) { | |
case _Codes.init: | |
_sendPort = command.args?[0] as SendPort; | |
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; | |
_sendPort.send(_Command(_Codes.init, | |
args: [rootIsolateToken, _interpreter.address, _labels])); | |
case _Codes.ready: | |
_isReady = true; | |
case _Codes.busy: | |
_isReady = false; | |
case _Codes.result: | |
_isReady = true; | |
resultsStream.add(command.args?[0] as Map<String, dynamic>); | |
default: | |
debugPrint('Detector unrecognized command: ${command.code}'); | |
} | |
} | |
void stop() { | |
_isolate.kill(); | |
} | |
} | |
class _DetectorServer { | |
int mlModelInputSize = 640; | |
Interpreter? _interpreter; | |
List<String>? _labels; | |
_DetectorServer(this._sendPort); | |
final SendPort _sendPort; | |
static void _run(SendPort sendPort) { | |
ReceivePort receivePort = ReceivePort(); | |
final _DetectorServer server = _DetectorServer(sendPort); | |
receivePort.listen((message) async { | |
final _Command command = message as _Command; | |
await server._handleCommand(command); | |
}); | |
sendPort.send(_Command(_Codes.init, args: [receivePort.sendPort])); | |
} | |
Future<void> _handleCommand(_Command command) async { | |
switch (command.code) { | |
case _Codes.init: | |
RootIsolateToken rootIsolateToken = | |
command.args?[0] as RootIsolateToken; | |
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); | |
_interpreter = Interpreter.fromAddress(command.args?[1] as int); | |
mlModelInputSize = _interpreter?.getInputTensors().first.shape[1] ?? 640; | |
_labels = command.args?[2] as List<String>; | |
_sendPort.send(const _Command(_Codes.ready)); | |
case _Codes.detect: | |
_sendPort.send(const _Command(_Codes.busy)); | |
_convertCameraImage(command.args?[0] as CameraImage); | |
default: | |
debugPrint('_DetectorService unrecognized command ${command.code}'); | |
} | |
} | |
void _convertCameraImage(CameraImage cameraImage) { | |
var preConversionTime = DateTime.now().millisecondsSinceEpoch; | |
convertCameraImageToImage(cameraImage).then((image) { | |
if (image != null) { | |
if (Platform.isAndroid) { | |
image = image_lib.copyRotate(image, angle: 90); | |
} | |
final results = analyseImage(image, preConversionTime); | |
_sendPort.send(_Command(_Codes.result, args: [results])); | |
} | |
}); | |
} | |
Map<String, dynamic> analyseImage( | |
image_lib.Image? image, int preConversionTime) { | |
var conversionElapsedTime = | |
DateTime.now().millisecondsSinceEpoch - preConversionTime; | |
var preProcessStart = DateTime.now().millisecondsSinceEpoch; | |
final imageInput = image_lib.copyResize(image!, | |
width: mlModelInputSize, height: mlModelInputSize); | |
final imageMatrix = List.generate( | |
imageInput.height, | |
(y) => List.generate( | |
imageInput.width, | |
(x) { | |
final pixel = imageInput.getPixel(x, y); | |
return [pixel.rNormalized, pixel.gNormalized, pixel.bNormalized]; | |
}, | |
), | |
); | |
var preProcessElapsedTime = | |
DateTime.now().millisecondsSinceEpoch - preProcessStart; | |
var inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; | |
final output = _runInference(imageMatrix); | |
List<List<double>> rawOutput = (output.first as List).first as List<List<double>>; | |
List<int> idx = []; | |
List<String> cls = []; | |
List<List<double>> box = []; | |
List<double> conf = []; | |
final numOfLabels = _labels?.length ?? 0; | |
final count = numOfLabels + 4; | |
(idx, box, conf) = nms(rawOutput, count, confidenceThreshold: 0.1, iouThreshold: 0.4); | |
if (idx.isNotEmpty) { | |
cls = idx.map((e) => _labels![e]).toList(); | |
} | |
var inferenceElapsedTime = | |
DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; | |
var totalElapsedTime = | |
DateTime.now().millisecondsSinceEpoch - preConversionTime; | |
return { | |
"cls": cls, | |
"box": box, | |
"conf": conf, | |
"stats": <String, String>{ | |
'Conversion time:': conversionElapsedTime.toString(), | |
'Pre-processing time:': preProcessElapsedTime.toString(), | |
'Inference time:': inferenceElapsedTime.toString(), | |
'Total prediction time:': totalElapsedTime.toString(), | |
'Frame': '${image.width} X ${image.height}', | |
}, | |
}; | |
} | |
List<Object> _runInference(List<List<List<num>>> imageMatrix) { | |
final input = [imageMatrix]; | |
final numOut = _interpreter?.getOutputTensors().first.shape[0] ?? 1; | |
final numOut1 = _interpreter?.getOutputTensors().first.shape[1] ?? 1; | |
final numOut2 = _interpreter?.getOutputTensors().first.shape[2] ?? 1; | |
final outputs = List<num>.filled(numOut * numOut1 * numOut2, 0).reshape([numOut, numOut1, numOut2]); | |
var map = <int, Object>{}; | |
map[0] = outputs; | |
_interpreter!.runForMultipleInputs([input], map); | |
return map.values.toList(); | |
} | |
} |
댓글
댓글 쓰기