콘텐츠로 이동

React Native에서 YOLO 온디바이스 구현 — 심층 가이드

조사 날짜: 2026-05-16
대상: React Native (iOS + Android) — Flutter 아님
모델: YOLO11n, YOLO11s (Detection / Classification)
기존 리서치 참조: 52_yolo-mobile-implementation.md (iOS Core ML, Android TFLite 일반 구현)


React Native에서 YOLO 온디바이스 구현 가능한가?

섹션 제목: “React Native에서 YOLO 온디바이스 구현 가능한가?”

결론: 가능하다. 단 공식 SDK는 없으며, 서드파티 패키지 조합이 필요하다.

  • Ultralytics 공식 React Native 패키지: 존재하지 않는다. Flutter용 ultralytics_yolo 패키지는 있지만 React Native는 지원하지 않는다. (Ultralytics GitHub Discussion #22398, 2025년 기준 공식 답변 없음)
  • 현실적으로 작동하는 접근법 2가지:
    1. react-native-fast-tflite + react-native-vision-camera + vision-camera-resize-plugin — 가장 검증된 조합. 실시간 카메라 추론 가능. v3.0.1 (2026-04-21) 기준 활발히 유지보수 중
    2. onnxruntime-react-native — 단일 이미지 추론에 적합. 실시간은 성능 한계 있음
  • 권장: 접근법 A (react-native-fast-tflite + VisionCamera) — 실시간 카메라 + GPU 가속 + 크로스플랫폼 단일 코드베이스

접근법난이도실시간 성능유지보수 상태iOSAndroid권장
A: fast-tflite + VisionCamera우수 (GPU 가속, 60FPS 가능)활발 (v3.0.1, 2026-04)CoreML delegateGPU/NNAPI delegate1순위
B: onnxruntime-react-native보통 (단일 이미지 ~200-500ms, CPU)활발 (Microsoft, 2026-03 업데이트)CoreML EPNNAPI/XNNPACK EP2순위 (단일 이미지)
C: Expo Native Module (Swift CoreML)최고 (Neural Engine 풀 활용)직접 관리최적별도 구현 필요3순위 (iOS 전용)
D: Native Module (Turbo/Nitro)최고직접 관리최적최적3순위 (팀 역량 있을 때)
E: 서버 API 하이브리드네트워크 지연 (~500ms+)N/A모두모두오프라인 불가 시 폴백

접근법 A: react-native-fast-tflite + VisionCamera (권장)

섹션 제목: “접근법 A: react-native-fast-tflite + VisionCamera (권장)”
카메라 프레임 (VisionCamera)
→ vision-camera-resize-plugin (GPU 가속 리사이즈, YUV→RGB 변환)
→ react-native-fast-tflite (JSI 동기 추론, CoreML/GPU delegate)
→ 출력 텐서 파싱 (JS)
→ Skia 오버레이 렌더링

핵심: Frame Processor는 JSI(JavaScript Interface)로 실행되어 Bridge를 거치지 않는다. 네이티브 스레드에서 동기 실행 → 60FPS 달성 가능.

Terminal window
# 핵심 패키지
yarn add react-native-fast-tflite react-native-nitro-modules
yarn add react-native-vision-camera react-native-vision-camera-worklets react-native-worklets
yarn add vision-camera-resize-plugin
# iOS
npx pod-install
metro.config.js
const { getDefaultConfig } = require('@react-native/metro-config');
const config = getDefaultConfig(__dirname);
// .tflite 파일을 에셋으로 처리
config.resolver.assetExts.push('tflite');
module.exports = config;
# Python (서버/PC에서 실행)
from ultralytics import YOLO
model = YOLO("yolo11n.pt")
# 식물 병 분류 모델
model.export(
format="tflite",
imgsz=320, # 모바일 최적화: 640 대신 320 권장
half=True, # FP16 양자화
# int8=True, # INT8 더 작고 빠름 (정확도 약간 손실)
)
# 출력: yolo11n_float16.tflite (~3MB)
# INT8 양자화 (최고 성능, 보급형 기기)
model.export(
format="tflite",
imgsz=320,
int8=True,
data="plant_disease.yaml", # 대표 데이터셋 필요
)
# 출력: yolo11n_int8.tflite (~1.5MB)

중요: YOLO11n TFLite 출력 텐서 형상

  • Detection: [1, (4+num_classes), num_anchors] — 예: [1, 84, 2100] (320px input)
  • Classification: [1, num_classes] — 예: [1, 38] (PlantVillage 38클래스)

실시간 카메라 추론 — 전체 구현 코드

섹션 제목: “실시간 카메라 추론 — 전체 구현 코드”
PlantDetectionScreen.tsx
import React, { useCallback, useEffect, useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
Camera,
useCameraDevice,
useFrameProcessor,
runAtTargetFps,
} from 'react-native-vision-camera';
import { useRunOnJS } from 'react-native-worklets';
import { useTensorflowModel } from 'react-native-fast-tflite';
import { NitroModules } from 'react-native-nitro-modules';
import { resize } from 'vision-camera-resize-plugin';
// 모델 설정
const INPUT_SIZE = 320;
const NUM_CLASSES = 38; // PlantVillage 클래스 수
const CONFIDENCE_THRESHOLD = 0.45;
const IOU_THRESHOLD = 0.5;
const TARGET_FPS = 10; // 실시간 10FPS로 제한 (배터리 절약)
interface Detection {
x: number;
y: number;
width: number;
height: number;
classId: number;
confidence: number;
}
interface DetectionResult {
detections: Detection[];
inferenceTimeMs: number;
}
export function PlantDetectionScreen() {
const device = useCameraDevice('back');
// iOS: core-ml delegate, Android: android-gpu delegate
const model = useTensorflowModel(
require('../assets/models/yolo11n_plant_float16.tflite'),
__DEV__ ? [] : ['core-ml'] // iOS 프로덕션에서 CoreML 활성화
);
// Nitro box: 워크릿에서 모델 접근하기 위해 필요
const boxedModel = useMemo(
() => (model.model != null ? NitroModules.box(model.model) : undefined),
[model.model]
);
const [result, setResult] = React.useState<DetectionResult | null>(null);
// JS 스레드에서 UI 업데이트 (워크릿에서 직접 setState 불가)
const onDetected = useRunOnJS(
(detections: Detection[], inferenceTimeMs: number) => {
setResult({ detections, inferenceTimeMs });
},
[]
);
const frameProcessor = useFrameProcessor(
(frame) => {
'worklet';
if (boxedModel == null) return;
runAtTargetFps(TARGET_FPS, () => {
'worklet';
const tflite = boxedModel.unbox();
const start = performance.now();
// 1. 프레임 리사이즈 (GPU 가속)
const resized = resize(frame, {
scale: { width: INPUT_SIZE, height: INPUT_SIZE },
pixelFormat: 'rgb',
dataType: 'float32', // FP16 모델이라도 float32로 입력
});
// 2. 추론 (동기, JSI)
const outputs = tflite.runSync([resized]);
const inferenceTimeMs = performance.now() - start;
// 3. 출력 파싱 — YOLO11n Detection [1, 84, 2100] → transposed
// fast-tflite는 평탄화된 Float32Array 반환
const output = new Float32Array(outputs[0]);
const numAnchors = 2100; // 320px input: 40*40 + 20*20 + 10*10 = 2100
const numAttrs = 4 + NUM_CLASSES; // cx, cy, w, h + classes
const detections: Detection[] = [];
for (let i = 0; i < numAnchors; i++) {
// YOLO11 TFLite: [1, numAttrs, numAnchors] 형태로 전치됨
// attr_idx * numAnchors + anchor_idx
const cx = output[0 * numAnchors + i];
const cy = output[1 * numAnchors + i];
const w = output[2 * numAnchors + i];
const h = output[3 * numAnchors + i];
// 클래스별 confidence 중 최대값 찾기
let maxConf = 0;
let classId = 0;
for (let c = 0; c < NUM_CLASSES; c++) {
const conf = output[(4 + c) * numAnchors + i];
if (conf > maxConf) {
maxConf = conf;
classId = c;
}
}
if (maxConf >= CONFIDENCE_THRESHOLD) {
detections.push({
x: (cx - w / 2) / INPUT_SIZE,
y: (cy - h / 2) / INPUT_SIZE,
width: w / INPUT_SIZE,
height: h / INPUT_SIZE,
classId,
confidence: maxConf,
});
}
}
// 4. NMS (간단한 greedy NMS)
const finalDetections = nmsSync(detections, IOU_THRESHOLD);
onDetected(finalDetections, inferenceTimeMs);
});
},
[boxedModel, onDetected]
);
if (device == null) return <Text>카메라 없음</Text>;
if (model.state === 'loading') return <Text>모델 로딩 중...</Text>;
if (model.state === 'error') return <Text>모델 오류: {model.error?.message}</Text>;
return (
<View style={styles.container}>
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
frameProcessor={frameProcessor}
pixelFormat="rgb"
/>
{result && (
<View style={styles.overlay}>
<Text style={styles.info}>
{result.detections.length} 감지 | {result.inferenceTimeMs.toFixed(1)}ms
</Text>
{/* Skia나 SVG로 바운딩 박스 오버레이 렌더링 */}
</View>
)}
</View>
);
}
// 워크릿 내에서 실행 가능한 동기 NMS
function nmsSync(detections: Detection[], iouThreshold: number): Detection[] {
'worklet';
const sorted = detections.sort((a, b) => b.confidence - a.confidence);
const result: Detection[] = [];
for (const det of sorted) {
let suppressed = false;
for (const kept of result) {
if (iou(det, kept) > iouThreshold) {
suppressed = true;
break;
}
}
if (!suppressed) result.push(det);
}
return result;
}
function iou(a: Detection, b: Detection): number {
'worklet';
const x1 = Math.max(a.x, b.x);
const y1 = Math.max(a.y, b.y);
const x2 = Math.min(a.x + a.width, b.x + b.width);
const y2 = Math.min(a.y + a.height, b.y + b.height);
const intersection = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
const union = a.width * a.height + b.width * b.height - intersection;
return intersection / union;
}
const styles = StyleSheet.create({
container: { flex: 1 },
overlay: { position: 'absolute', top: 20, left: 20 },
info: { color: 'white', fontSize: 14, backgroundColor: 'rgba(0,0,0,0.5)', padding: 4 },
});
# ios/Podfile
$EnableCoreMLDelegate = true
target 'YourApp' do
# ...
end
// app.json (Expo)
{
"expo": {
"plugins": [
[
"react-native-fast-tflite",
{
"enableAndroidGpuLibraries": true
}
]
]
}
}
<!-- android/app/src/main/AndroidManifest.xml (React Native CLI) -->
<uses-native-library android:name="libOpenCL.so" android:required="false"/>
<uses-native-library android:name="libOpenCL-car-halt.so" android:required="false"/>

접근법 B: onnxruntime-react-native (단일 이미지 추론)

섹션 제목: “접근법 B: onnxruntime-react-native (단일 이미지 추론)”

단일 사진 촬영 후 진단 (실시간 카메라 아님) 시나리오에 적합.

Terminal window
yarn add onnxruntime-react-native
cd ios && pod install && cd ..

주의: Expo Go에서는 동작하지 않음. expo prebuild + custom dev client 필요.

from ultralytics import YOLO
model = YOLO("yolo11n.pt")
model.export(
format="onnx",
imgsz=640,
simplify=True, # ONNX 그래프 단순화
opset=17,
)
# 출력: yolo11n.onnx (~5.4MB)
plantDiagnosis.ts
import * as ort from 'onnxruntime-react-native';
import { Image } from 'react-native';
const INPUT_SIZE = 640;
let session: ort.InferenceSession | null = null;
// 모델 초기화 (앱 시작 시 1회)
export async function initModel(): Promise<void> {
session = await ort.InferenceSession.create(
require('../assets/models/yolo11n_plant.onnx'),
{
executionProviders: ['coreml', 'cpu'], // iOS: CoreML 우선
// executionProviders: ['nnapi', 'cpu'], // Android: NNAPI 우선
}
);
console.log('ONNX 모델 로드 완료');
console.log('입력:', session.inputNames);
console.log('출력:', session.outputNames);
}
// 이미지 → Float32 NCHW 텐서 변환
async function imageToTensor(imageUri: string): Promise<ort.Tensor> {
// react-native-image-picker 등으로 얻은 URI 사용
// Skia 또는 canvas-based 라이브러리로 픽셀 추출 필요
// 여기서는 개념 코드 (실제 구현은 아래 주의사항 참조)
const { width, height, data } = await getImagePixels(imageUri, INPUT_SIZE, INPUT_SIZE);
// RGBA → NCHW Float32 변환
const float32 = new Float32Array(3 * INPUT_SIZE * INPUT_SIZE);
for (let i = 0; i < INPUT_SIZE * INPUT_SIZE; i++) {
float32[i] = data[i * 4] / 255.0; // R
float32[INPUT_SIZE * INPUT_SIZE + i] = data[i * 4 + 1] / 255.0; // G
float32[2 * INPUT_SIZE * INPUT_SIZE + i] = data[i * 4 + 2] / 255.0; // B
}
return new ort.Tensor('float32', float32, [1, 3, INPUT_SIZE, INPUT_SIZE]);
}
// 추론 실행
export async function runDiagnosis(imageUri: string) {
if (!session) throw new Error('모델 미초기화');
const inputTensor = await imageToTensor(imageUri);
const feeds = { images: inputTensor };
const start = Date.now();
const results = await session.run(feeds);
const latencyMs = Date.now() - start;
// YOLO11 ONNX 출력: output0 [1, 84, 8400] (640px input)
const output = results['output0'].data as Float32Array;
const detections = parseYOLO11Output(output, 8400, 80);
return { detections, latencyMs };
}
function parseYOLO11Output(
output: Float32Array,
numAnchors: number,
numClasses: number
): Detection[] {
const detections: Detection[] = [];
const numAttrs = 4 + numClasses;
for (let i = 0; i < numAnchors; i++) {
// [1, 84, 8400] → attr-major 순서
const cx = output[0 * numAnchors + i];
const cy = output[1 * numAnchors + i];
const w = output[2 * numAnchors + i];
const h = output[3 * numAnchors + i];
let maxConf = 0, classId = 0;
for (let c = 0; c < numClasses; c++) {
const conf = output[(4 + c) * numAnchors + i];
if (conf > maxConf) { maxConf = conf; classId = c; }
}
if (maxConf > 0.45) {
detections.push({
x: (cx - w / 2) / INPUT_SIZE,
y: (cy - h / 2) / INPUT_SIZE,
width: w / INPUT_SIZE,
height: h / INPUT_SIZE,
classId,
confidence: maxConf,
});
}
}
return nms(detections, 0.5);
}

이미지 픽셀 추출 방법: React Native에는 canvas.getImageData()가 없다.
옵션 1: @shopify/react-native-skiamakeImageFromEncoded + readPixels()
옵션 2: react-native-image-resizer + 커스텀 Native Module
옵션 3: expo-image-manipulator (Expo 환경)


접근법 C: Expo Native Module — iOS CoreML (가장 고성능, iOS 전용)

섹션 제목: “접근법 C: Expo Native Module — iOS CoreML (가장 고성능, iOS 전용)”

iOS에서 최고 성능이 필요할 때. Neural Engine 풀 활용.

Terminal window
npx create-expo-module plant-coreml-classifier --local

Swift 구현 (modules/plant-coreml-classifier/ios/PlantCoremlClassifier.swift)

섹션 제목: “Swift 구현 (modules/plant-coreml-classifier/ios/PlantCoremlClassifier.swift)”
import ExpoModulesCore
import CoreML
import Vision
import UIKit
public class PlantCoremlClassifier: Module {
private var model: VNCoreMLModel?
public func definition() -> ModuleDefinition {
Name("PlantCoremlClassifier")
AsyncFunction("loadModel") { (promise: Promise) in
guard let modelURL = Bundle.main.url(
forResource: "yolo11n_plant",
withExtension: "mlmodelc"
) else {
promise.reject("MODEL_NOT_FOUND", "모델 파일을 찾을 수 없습니다")
return
}
do {
let config = MLModelConfiguration()
config.computeUnits = .all // Neural Engine + GPU + CPU
let coreMLModel = try MLModel(contentsOf: modelURL, configuration: config)
self.model = try VNCoreMLModel(for: coreMLModel)
promise.resolve(true)
} catch {
promise.reject("LOAD_ERROR", error.localizedDescription)
}
}
AsyncFunction("classify") { (imageBase64: String, promise: Promise) in
guard let model = self.model else {
promise.reject("NOT_INITIALIZED", "모델 미초기화")
return
}
guard let imageData = Data(base64Encoded: imageBase64),
let uiImage = UIImage(data: imageData),
let cgImage = uiImage.cgImage else {
promise.reject("INVALID_IMAGE", "이미지 디코딩 실패")
return
}
let request = VNCoreMLRequest(model: model) { req, err in
if let err = err {
promise.reject("INFERENCE_ERROR", err.localizedDescription)
return
}
// Classification 결과 파싱
let results = req.results as? [VNClassificationObservation] ?? []
let top5 = results.prefix(5).map { obs in
["label": obs.identifier, "confidence": obs.confidence]
}
promise.resolve(top5)
}
request.imageCropAndScaleOption = .scaleFill
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch {
promise.reject("HANDLER_ERROR", error.localizedDescription)
}
}
}
}
}
modules/plant-coreml-classifier/index.ts
import { requireNativeModule } from 'expo-modules-core';
const PlantCoremlClassifier = requireNativeModule('PlantCoremlClassifier');
export async function loadModel(): Promise<boolean> {
return PlantCoremlClassifier.loadModel();
}
export async function classifyImage(imageUri: string): Promise<
Array<{ label: string; confidence: number }>
> {
// URI → Base64 변환
const response = await fetch(imageUri);
const blob = await response.blob();
const base64 = await blobToBase64(blob);
return PlantCoremlClassifier.classify(base64);
}
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
const result = reader.result as string;
resolve(result.split(',')[1]);
};
reader.readAsDataURL(blob);
});
}

접근법 D: 서버 API 하이브리드 (온디바이스 폴백 패턴)

섹션 제목: “접근법 D: 서버 API 하이브리드 (온디바이스 폴백 패턴)”

온디바이스 실패 또는 구형 기기 대응 시 사용.

plantDiagnosisService.ts
export async function diagnose(imageUri: string): Promise<DiagnosisResult> {
// 1차 시도: 온디바이스
try {
const onDeviceResult = await runOnDeviceInference(imageUri);
if (onDeviceResult.confidence > 0.6) {
return { ...onDeviceResult, source: 'on-device' };
}
// 신뢰도 낮으면 서버로 폴백
} catch (e) {
console.warn('온디바이스 추론 실패, 서버 폴백:', e);
}
// 2차: 서버 API
try {
const formData = new FormData();
formData.append('image', { uri: imageUri, type: 'image/jpeg', name: 'plant.jpg' } as any);
const response = await fetch('https://api.plantdiagnosis.example/detect', {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(10000), // 10초 타임아웃
});
const data = await response.json();
return { ...data, source: 'server' };
} catch (e) {
throw new Error('온디바이스 및 서버 모두 실패');
}
}

방법 1: 앱 번들에 직접 포함 (소규모 모델, 권장)

섹션 제목: “방법 1: 앱 번들에 직접 포함 (소규모 모델, 권장)”

Android (.tflite)

android/app/src/main/assets/models/yolo11n_plant.tflite

android/app/build.gradle:

android {
aaptOptions {
noCompress "tflite" // 압축 안 함 (필수!)
}
}

React Native 코드에서:

// require() 방식 — Metro가 번들에 포함
const model = await loadTensorflowModel(
require('./assets/models/yolo11n_plant.tflite'),
['android-gpu']
);

iOS (.tflite)

Xcode에서 yolo11n_plant.tfliteCopy Bundle Resources에 추가하거나, 동일하게 require()로 접근.

iOS (.mlmodelc, Expo Native Module 방식)

your-module.podspec
s.resources = ["**/*.mlmodelc", "**/*.mlpackage"]

모델 컴파일:

Terminal window
# .mlpackage → .mlmodelc 컴파일
xcrun coremlcompiler compile yolo11n_plant.mlpackage ios/
# 결과: yolo11n_plant.mlmodelc/ (디렉토리)

방법 2: 런타임 다운로드 (대형 모델, OTA 업데이트)

섹션 제목: “방법 2: 런타임 다운로드 (대형 모델, OTA 업데이트)”
modelManager.ts
import RNFS from 'react-native-fs';
const MODEL_URL = 'https://cdn.example.com/models/yolo11s_plant_v2.tflite';
const MODEL_PATH = `${RNFS.DocumentDirectoryPath}/yolo11s_plant.tflite`;
const MODEL_VERSION_KEY = 'model_version';
const CURRENT_MODEL_VERSION = '2.1.0';
export async function ensureModelReady(): Promise<string> {
const cachedVersion = await AsyncStorage.getItem(MODEL_VERSION_KEY);
if (cachedVersion === CURRENT_MODEL_VERSION && await RNFS.exists(MODEL_PATH)) {
console.log('캐시된 모델 사용:', MODEL_PATH);
return MODEL_PATH;
}
// 다운로드
console.log('모델 다운로드 시작...');
const { promise } = RNFS.downloadFile({
fromUrl: MODEL_URL,
toFile: MODEL_PATH,
progressDivider: 10,
begin: (res) => console.log('다운로드 시작, 크기:', res.contentLength),
progress: (res) => {
const progress = (res.bytesWritten / res.contentLength) * 100;
console.log(`다운로드 ${progress.toFixed(1)}%`);
},
});
await promise;
await AsyncStorage.setItem(MODEL_VERSION_KEY, CURRENT_MODEL_VERSION);
return `file://${MODEL_PATH}`;
}
// 사용
const modelPath = await ensureModelReady();
const model = await loadTensorflowModel({ url: modelPath }, ['core-ml']);
모델크기번들 포함OTA 다운로드
YOLO11n FP32 TFLite~5.4MB가능불필요
YOLO11n FP16 TFLite~2.7MB권장불필요
YOLO11n INT8 TFLite~1.4MB권장불필요
YOLO11s FP16 TFLite~9MB가능선택
YOLO11m FP16 TFLite~18MB주의권장

기기모델입력Delegate추론 시간
iPhone 15 Pro (A17)YOLO11n FP16320×320CoreML~8-15ms
iPhone 12 (A14)YOLO11n FP16320×320CoreML~15-25ms
Pixel 8 (Tensor G3)YOLO11n INT8320×320GPU~12-20ms
Samsung S23 (SD 8 Gen 2)YOLO11n INT8320×320GPU~10-18ms
보급형 Android (SD 778G)YOLO11n INT8320×320GPU~25-50ms
문제 케이스YOLOv8n FP32640×640CPU~200ms

640×640 입력이 핵심 병목: 640px 대신 320px 사용 시 ~4배 빠름.
GPU delegate: CPU 대비 3-5배 빠름 (TFLite 공식 자료).
Frame Processor 전체 파이프라인 (리사이즈 + 추론 + 파싱): 60FPS = 16.7ms 예산.
→ YOLO11n INT8 + 320px + GPU delegate: 10-20FPS 실시간 추론 현실적

환경모델추론 시간
iPhone 15 Pro, CoreML EPYOLO11n ONNX 640px~50-80ms
Android, NNAPI EPYOLO11n ONNX 640px~100-200ms
Android, CPU EPYOLO11n ONNX 640px~300-600ms

ONNX Runtime은 실시간 카메라(30FPS 이상)에 부적합. 단일 이미지 진단(사진 촬영)에 적합.

  • Frame Processor 자체 오버헤드: ~0ms (JSI 동기 실행, Bridge 없음)
  • vision-camera-resize-plugin (GPU 가속): ~1-3ms
  • 전체 파이프라인: 추론 시간 지배적, Processor 오버헤드 무시 가능
  • runAtTargetFps(10)으로 제한 시 배터리 소모 대폭 감소

이슈 1: 640×640 입력 시 200ms+ 지연 (심각)

섹션 제목: “이슈 1: 640×640 입력 시 200ms+ 지연 (심각)”

증상: YOLOv8n/YOLO11n을 640px 입력으로 실행 시 ~200ms 추론 → 5FPS 이하
해결: 입력 크기를 320px로 낮춤. imgsz=320으로 재export 또는 vision-camera-resize-plugin에서 320×320으로 리사이즈

# 해결: 320px로 export
model.export(format="tflite", imgsz=320, half=True)

이슈 2: Android GPU Delegate에서 신뢰도 0에 가까운 값 출력 (심각)

섹션 제목: “이슈 2: Android GPU Delegate에서 신뢰도 0에 가까운 값 출력 (심각)”

증상: YOLO11n TFLite를 Android GPU delegate로 실행 시 confidence ~0.000007 출력
원인: letterbox 없이 단순 resize + 정규화 누락
해결:

// 잘못된 방법
resize(frame, { scale: { width: 320, height: 320 }, pixelFormat: 'rgb', dataType: 'uint8' })
// 올바른 방법: float32 + letterbox 보장
resize(frame, { scale: { width: 320, height: 320 }, pixelFormat: 'rgb', dataType: 'float32' })
// vision-camera-resize-plugin은 scale 옵션으로 자동 letterbox 적용 (v2.x 이후)

이슈 3: NNAPI Android 15에서 deprecated

섹션 제목: “이슈 3: NNAPI Android 15에서 deprecated”

'nnapi' delegate는 Android 15 (API 35)부터 deprecated.
해결: 'android-gpu' delegate 사용.

// Android 버전별 delegate 선택
import { Platform } from 'react-native';
const delegate = Platform.select({
ios: ['core-ml'],
android: parseInt(Platform.Version as string) >= 35 ? ['android-gpu'] : ['nnapi'],
});

이슈 4: Expo Go에서 onnxruntime-react-native 미동작

섹션 제목: “이슈 4: Expo Go에서 onnxruntime-react-native 미동작”

onnxruntime-react-native는 native module이므로 Expo Go에서 실행 불가.
해결: expo prebuild + eas build + custom dev client 사용.

이슈 5: YOLO11 TFLite 출력 텐서 형상 혼란

섹션 제목: “이슈 5: YOLO11 TFLite 출력 텐서 형상 혼란”

모델 크기와 입력 해상도에 따라 형상이 다름:

입력출력 형상 (Detection)anchors 수
640×640[1, 84, 8400]8400
320×320[1, 84, 2100]2100
224×224[1, 84, 1029]1029

클래스 수가 80이 아닌 경우 (예: PlantVillage 38클래스):
[1, 42, 2100] (4+38=42)

Netron으로 모델 구조 사전 확인 필수: netron.app

이슈 6: VisionCamera v4 아카이브 상태

섹션 제목: “이슈 6: VisionCamera v4 아카이브 상태”

react-native-vision-camera V4는 archived (더 이상 유지보수 없음).
현재 활성 버전: V5 (margelo 조직에서 관리, 문서: visioncamera.margelo.com)
fast-tflite v3.x는 V5 API 기준. V4 사용 중이라면 V5로 마이그레이션 필요.

이슈 7: 식물 병 Classification vs Detection 선택

섹션 제목: “이슈 7: 식물 병 Classification vs Detection 선택”
시나리오권장 태스크모델
”이 잎은 어떤 병?” — 잎 전체 사진Classificationyolo11n-cls
”병변 위치를 보여줘” — 감지 + 위치Detectionyolo11n
여러 잎 동시 감지Detectionyolo11s

Classification은 Detection보다 출력 파싱이 단순하고 (softmax 1개 벡터), 속도도 빠름. 식물 병 진단 MVP에는 Classification 먼저 구현 권장.


onnxruntime-react-native + YOLO11n-cls ONNX
→ 사진 촬영/갤러리 선택 → 추론 → 결과 텍스트 표시
→ Skia로 픽셀 추출 문제 해결
react-native-fast-tflite + VisionCamera V5 + resize-plugin
→ 카메라 피드에서 실시간 Classification
→ GPU delegate 최적화
→ Skia 오버레이 (바운딩 박스)
iOS: Expo Native Module + Swift CoreML (Neural Engine 최적화)
Android: fast-tflite INT8 + 320px 튜닝
OTA 모델 업데이트 (RNFS + CDN)

  1. react-native-fast-tflite GitHub — v3.0.1 공식 문서
  2. react-native-vision-camera 공식 문서
  3. onnxruntime-react-native 공식 문서
  4. Ultralytics GitHub Discussion #22398 — Has anyone successfully called YOLO using React Native?
  5. Marc Rousavy — Reinventing Camera Processing (TFLite GPU from JavaScript)
  6. Ultralytics YOLO TFLite Export Guide
  7. Julius Hietala — Building a React Native CoreML App with Expo and YOLOv8
  8. Ultralytics Community — Low confidence scores with YOLO11n TFLite + Android GPU delegate
  9. Simplico — How to Use an ONNX Model in React Native (2026-01)
  10. react-native-fast-tflite Issue #96 — Long latency using YOLO TFLite
  11. YuZou Medium — The perfect combination of fast-tflite and VisionCamera
  12. Simform Engineering — Object Detection in React Native