Vision & AI 모델 세부 설명
작성일: 2025-08
문서 버전: v 1.0
시리즈: 3/4 — 비전 모델 선정 · 데이터셋 · 학습 계획
1. 온디바이스 AI 파이프라인 개요
본 앱의 모든 추론 연산은 기기 내에서 처리한다. 네트워크 없이도 동작하며, 프레임당 처리 지연을 최소화하는 것이 핵심 설계 목표다.
1.1 파이프라인 흐름
CameraX Frame (1280×720, RGBA)
│
▼
┌─────────────────────────────────────────────────────┐
│ :ml-core │
│ │
│ ┌──────────────┐ ┌───────────────┐ │
│ │BreedClassifier│ │ PoseEstimator │ │
│ │ TFLite │ │ MediaPipe │ │
│ │ EfficientNet │ │ Holistic │ │
│ └──────┬────────┘ └──────┬────────┘ │
│ │ │ │
│ ┌──────▼────────┐ ┌──────▼────────┐ │
│ │BreedResult │ │PoseLandmarks │ │
│ │ (breed, conf) │ │ (17 keypoints)│ │
│ └──────┬────────┘ └──────┬────────┘ │
│ │ │ │
│ │ ┌────────────▼──────────┐ │
│ │ │ MotionAnalyzer │ │
│ │ │ OpenCV C++ (JNI) │ │
│ │ │ Lucas-Kanade OF │ │
│ │ └────────────┬──────────┘ │
│ │ │ │
│ └──────────┬────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │HealthScorer │ │
│ │ ONNX Runtime│ │
│ └──────┬──────┘ │
└─────────────────────│───────────────────────────────┘
│
▼
AnalysisResult (건강 점수 + 행동 분류)
1.2 모델별 처리 방식
| 모델 | 실행 방식 | 스레드 전략 |
|---|---|---|
| BreedClassifier | 캡처 시 1회 추론 | IO Dispatcher |
| PoseEstimator | 실시간 (LIVE_STREAM) | MediaPipe 내부 스레드 |
| MotionAnalyzer | 프레임마다 C++ 처리 | NDK 전용 스레드 |
| HealthScorer | 위 3개 결과 취합 후 1회 | Default Dispatcher |
1.3 하드웨어 가속 전략
Android 기기
│
├── GPU Delegate (TFLite) ← 종 분류 모델
├── GPU Delegate (MediaPipe) ← 자세 추정
├── NNAPI Delegate (ONNX RT) ← 건강 점수 (API 27+)
└── C++ NEON SIMD (OpenCV) ← 광학 흐름 (ARM 최적화)
폴백 순서: GPU → NNAPI → CPU (기기 지원에 따라 자동 선택)
2. 종 분류 모델 (Breed Classifier)
2.1 모델 선정
선정 모델: EfficientNet-Lite 4 (TFLite)
| 후보 모델 | 정확도 | 모델 크기 | 추론 속도 (모바일) | 선정 여부 |
|---|---|---|---|---|
| EfficientNet-Lite 4 | Top-1 80.4% | 16.0 MB | ~180 ms (CPU) | ✅ 선정 |
| MobileNetV 3-Large | Top-1 75.2% | 5.4 MB | ~60 ms (CPU) | 보조 후보 |
| ResNet-50 | Top-1 76.1% | 98 MB | ~380 ms (CPU) | ❌ 모바일 부적합 |
| EfficientNet-B 0 | Top-1 77.1% | 20 MB | ~200 ms (CPU) | 차순위 |
선정 이유
- TFLite 공식 최적화 버전 존재 — 별도 변환 없이 바로 사용 가능
- GPU Delegate 적용 시 약 40~50 ms로 단축 → 실용적 속도 달성
- 고양이 품종처럼 세밀한 특징(Fine-grained) 분류에 충분한 표현력
- MobileNetV 3 대비 정확도 우위, ResNet 대비 크기·속도 우위
단계별 전략
- Phase 1: 공개 사전학습 모델 + Transfer Learning (빠른 MVP)
- Phase 2: 자체 수집 데이터로 Fine-tuning (정확도 고도화)
- Phase 3: 경량화 — 양자화(INT 8) 적용으로 크기 절반, 속도 2× 향상
2.2 데이터셋
기본 데이터셋
| 데이터셋 | 품종 수 | 이미지 수 | 라이선스 | 용도 |
|---|---|---|---|---|
| Oxford-IIIT Pet Dataset | 37종 (고양이 12종) | 7,349장 | CC BY-SA 4.0 | 기본 학습 |
| Cat Breeds Dataset (Kaggle) | 67종 | 15,746장 | 공개 | 학습 보완 |
| iNaturalist (고양이 subset) | 30종+ | 10,000장+ | CC BY-NC | 다양성 확보 |
데이터 수집 확장 계획
1단계 (MVP): Oxford-IIIT + Kaggle 데이터셋 병합
→ 약 50종, 20,000장 기준 학습
2단계: 웹 크롤링 + 사용자 제공 이미지 (앱 내 선택적 업로드)
→ 한국 인기 품종 추가 (코리안 숏헤어, 러시안 블루 등)
→ 목표: 70종, 50,000장
3단계: 앱 배포 후 사용자 피드백 기반 오분류 샘플 재학습 (Active Learning)
데이터 전처리 파이프라인
# preprocess.py
import tensorflow as tf
IMG_SIZE = 224 # EfficientNet-Lite4 입력 크기
def preprocess_image(image_path: str, label: int):
img = tf.io.read_file(image_path)
img = tf.image.decode_jpeg(img, channels=3)
img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE])
img = tf.cast(img, tf.float32) / 255.0 # [0, 1] 정규화
img = (img - [0.485, 0.456, 0.406]) \
/ [0.229, 0.224, 0.225] # ImageNet 평균/표준편차
return img, label
def augment(image, label):
image = tf.image.random_flip_left_right(image)
image = tf.image.random_brightness(image, 0.2)
image = tf.image.random_contrast(image, 0.8, 1.2)
image = tf.image.random_saturation(image, 0.8, 1.2)
# 랜덤 회전 (±15도)
image = tfa.image.rotate(image,
tf.random.uniform([], -0.26, 0.26))
return image, label
2.3 학습 계획
환경
학습 환경: Google Colab Pro+ (A100 GPU) 또는 로컬 RTX 3090
프레임워크: TensorFlow 2.x + Keras
변환 도구: TFLite Converter
Transfer Learning 절차
# train_breed_classifier.py
import tensorflow as tf
BASE_MODEL_URL = "https://tfhub.dev/google/efficientnet/lite4/feature-vector/2"
NUM_CLASSES = 67 # 목표 품종 수
def build_model():
base = tf.keras.applications.EfficientNetLite4(
include_top=False,
weights="imagenet",
input_shape=(224, 224, 3)
)
# 1단계: Feature Extractor 고정, 분류 헤드만 학습
base.trainable = False
model = tf.keras.Sequential([
base,
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dropout(0.3),
tf.keras.layers.Dense(NUM_CLASSES, activation="softmax")
])
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss="categorical_crossentropy",
metrics=["accuracy", "top_5_accuracy"]
)
return model
# 2단계: 상위 레이어 일부 해제 후 Fine-tuning
def fine_tune(model, unfreeze_from: int = -30):
model.layers[0].trainable = True
for layer in model.layers[0].layers[:unfreeze_from]:
layer.trainable = False
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-5), # 낮은 학습률
loss="categorical_crossentropy",
metrics=["accuracy"]
)
TFLite 변환 및 양자화
# convert_to_tflite.py
# Float32 변환 (기본)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
# INT8 양자화 (권장 — 크기 1/4, 속도 2×)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_data_gen
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS_INT8
]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
quantized_model = converter.convert()
with open("cat_breed_int8.tflite", "wb") as f:
f.write(quantized_model)
목표 성능 지표
| 지표 | MVP 목표 | 고도화 목표 |
|---|---|---|
| Top-1 Accuracy | ≥ 70% | ≥ 85% |
| Top-5 Accuracy | ≥ 90% | ≥ 95% |
| 모델 크기 (INT 8) | ≤ 8 MB | ≤ 6 MB |
| 추론 속도 (GPU) | ≤ 200 ms | ≤ 100 ms |
3. 자세 추정 모델 (Pose Estimator)
3.1 모델 선정
선정 모델: MediaPipe Pose Landmarker (Lite)
| 후보 | 랜드마크 수 | 모바일 최적화 | 동물 지원 | 선정 여부 |
|---|---|---|---|---|
| MediaPipe Pose Landmarker Lite | 33개 | ✅ 공식 지원 | 사람 기준, 적용 가능 | ✅ 선정 |
| MediaPipe Pose Landmarker Full | 33개 | ✅ | 사람 기준 | 보조 (정밀도 필요 시) |
| OpenPose (TFLite 변환) | 18개 | 비공식 | 사람 기준 | ❌ 변환 불안정 |
| DeepLabCut (동물 전용) | 커스텀 | ❌ 모바일 미지원 | ✅ 동물 전용 | ❌ 모바일 부적합 |
선정 이유 및 적용 전략
MediaPipe는 사람 자세 추정 모델이지만, 고양이의 체형(4지 보행, 척추 굴곡)에서 유의미한 특징점을 추출할 수 있다. 특히 다음 랜드마크가 고양이 건강 분석에 활용 가능하다.
사람 랜드마크 → 고양이 적용 매핑
어깨 (11, 12) → 앞다리 어깨 관절
팔꿈치 (13, 14) → 앞다리 무릎
손목 (15, 16) → 앞발 위치
엉덩이 (23, 24) → 뒷다리 골반
무릎 (25, 26) → 뒷다리 무릎
발목 (27, 28) → 뒷발 위치
코 (0) → 머리 위치
Phase 2 이후: 고양이 전용 자세 데이터셋으로 커스텀 Pose Landmarker 모델 학습 (MediaPipe Model Maker 활용).
3.2 MediaPipe 통합 코드
// CatPoseEstimator.kt (:ml-core)
class CatPoseEstimator @Inject constructor(
@ApplicationContext private val context: Context,
private val listener: PoseResultListener
) {
interface PoseResultListener {
fun onResult(result: PoseLandmarkerResult, inputImage: MPImage)
fun onError(error: RuntimeException)
}
private val poseLandmarker: PoseLandmarker by lazy {
PoseLandmarker.createFromOptions(
context,
PoseLandmarker.PoseLandmarkerOptions.builder()
.setBaseOptions(
BaseOptions.builder()
.setModelAssetPath("pose_landmarker_lite.task")
.setDelegate(Delegate.GPU)
.build()
)
.setRunningMode(RunningMode.LIVE_STREAM)
.setNumPoses(1) // 고양이 1마리 기준
.setMinPoseDetectionConfidence(0.5f)
.setMinTrackingConfidence(0.5f)
.setResultListener(listener::onResult)
.setErrorListener(listener::onError)
.build()
)
}
fun detectAsync(imageProxy: ImageProxy) {
val mpImage = BitmapImageBuilder(imageProxy.toBitmap()).build()
poseLandmarker.detectAsync(mpImage, imageProxy.imageInfo.timestamp)
}
fun close() = poseLandmarker.close()
}
3.3 자세 기반 건강 지표 계산
// PoseHealthAnalyzer.kt
object PoseHealthAnalyzer {
// 척추 굴곡 각도: 웅크림 여부 판단
fun calculateSpineCurvature(landmarks: List<NormalizedLandmark>): Float {
val shoulder = landmarks[11].toVector()
val hip = landmarks[23].toVector()
val nose = landmarks[0].toVector()
// 머리-어깨-골반 각도 계산
return angleBetweenVectors(nose - shoulder, hip - shoulder)
}
// 보행 비대칭 점수: 절뚝거림 감지
fun calculateGaitAsymmetry(
leftAnkle: NormalizedLandmark,
rightAnkle: NormalizedLandmark
): Float {
val leftHeight = leftAnkle.y()
val rightHeight = rightAnkle.y()
return abs(leftHeight - rightHeight) / ((leftHeight + rightHeight) / 2f)
}
// 활동 반경: 프레임 간 관절 이동 거리 합산
fun calculateActivityRadius(
prev: List<NormalizedLandmark>,
curr: List<NormalizedLandmark>
): Float = KEY_JOINTS.sumOf { idx ->
val dx = (curr[idx].x() - prev[idx].x()).toDouble()
val dy = (curr[idx].y() - prev[idx].y()).toDouble()
sqrt(dx * dx + dy * dy)
}.toFloat()
private val KEY_JOINTS = listOf(0, 11, 12, 13, 14, 23, 24, 25, 26)
private fun angleBetweenVectors(v1: Vector2D, v2: Vector2D): Float {
val dot = v1.x * v2.x + v1.y * v2.y
val mag = v1.magnitude() * v2.magnitude()
return if (mag == 0f) 0f else acos((dot / mag).coerceIn(-1f, 1f))
}
}
4. 움직임 분석 — OpenCV C++ (NDK)
4.1 기술 선택 근거
Java/Kotlin으로 광학 흐름 연산을 구현했을 때와 C++ NDK 구현의 성능 차이는 다음과 같다.
| 구현 방식 | 처리 시간 (1280×720) | FPS | 선택 |
|---|---|---|---|
| Kotlin + OpenCV Android SDK | ~85 ms/frame | ~12 FPS | ❌ |
| C++ NDK + OpenCV (NEON SIMD) | ~18 ms/frame | ~55 FPS | ✅ |
| C++ NDK + CUDA (GPU) | ~8 ms/frame | ~125 FPS | 참고 (NDK에서 제한적) |
실시간 분석을 위한 최소 요건(24 FPS)을 충족하려면 C++ NDK가 필수다.
4.2 CMake 빌드 설정
# ml/ml-core/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project(motion_analyzer)
# OpenCV Android SDK 경로 (로컬 설치)
set(OpenCV_DIR "${CMAKE_SOURCE_DIR}/../../../../../opencv/sdk/native/jni")
find_package(OpenCV REQUIRED)
# ARM NEON SIMD 최적화 활성화
if(ANDROID_ABI STREQUAL "arm64-v8a")
add_compile_options(-O3 -march=armv8-a+simd)
endif()
add_library(
motion_analyzer
SHARED
motion_analyzer.cpp
optical_flow_processor.cpp
behavior_detector.cpp
)
target_include_directories(motion_analyzer PRIVATE
${OpenCV_INCLUDE_DIRS}
)
target_link_libraries(motion_analyzer
${OpenCV_LIBS}
android
log
)
4.3 C++ 핵심 구현
// optical_flow_processor.cpp
#include "optical_flow_processor.h"
#include <opencv2/video/tracking.hpp>
#include <android/log.h>
#define LOG_TAG "MotionAnalyzer"
OpticalFlowResult OpticalFlowProcessor::compute(
const cv::Mat& prevGray,
const cv::Mat& currGray) {
// 1. Shi-Tomasi 특징점 검출
std::vector<cv::Point2f> prevPts;
cv::goodFeaturesToTrack(
prevGray,
prevPts,
MAX_CORNERS, // 최대 200개 특징점
QUALITY_LEVEL, // 0.01
MIN_DISTANCE, // 10px 최소 거리
cv::noArray(),
3, // blockSize
false,
0.04 // Harris detector k
);
if (prevPts.empty()) return {0.0f, 0.0f, 0};
// 2. Lucas-Kanade 피라미드 광학 흐름
std::vector<cv::Point2f> currPts;
std::vector<uchar> status;
std::vector<float> err;
cv::calcOpticalFlowPyrLK(
prevGray, currGray,
prevPts, currPts,
status, err,
cv::Size(21, 21), // 윈도우 크기
3, // 피라미드 레벨
cv::TermCriteria(
cv::TermCriteria::COUNT | cv::TermCriteria::EPS,
30, 0.01
)
);
// 3. 유효 벡터 필터링 및 통계 산출
float totalMagnitude = 0.0f;
float maxMagnitude = 0.0f;
int validCount = 0;
for (size_t i = 0; i < status.size(); ++i) {
if (!status[i]) continue;
float dx = currPts[i].x - prevPts[i].x;
float dy = currPts[i].y - prevPts[i].y;
float mag = std::sqrt(dx * dx + dy * dy);
totalMagnitude += mag;
maxMagnitude = std::max(maxMagnitude, mag);
++validCount;
}
return {
validCount > 0 ? totalMagnitude / validCount : 0.0f, // 평균 이동량
maxMagnitude, // 최대 이동량
validCount // 유효 특징점 수
};
}
// behavior_detector.cpp
#include "behavior_detector.h"
BehaviorTag BehaviorDetector::classify(
const std::vector<OpticalFlowResult>& recentFrames,
float spineCurvature,
float gaitAsymmetry) {
// 최근 N프레임 평균 활동량
float avgMotion = 0.0f;
for (const auto& f : recentFrames) avgMotion += f.avgMagnitude;
avgMotion /= static_cast<float>(recentFrames.size());
// 규칙 기반 1차 분류
if (gaitAsymmetry > GAIT_ASYMMETRY_THRESHOLD) {
return BehaviorTag::LIMPING; // 절뚝거림
}
if (avgMotion < LOW_ACTIVITY_THRESHOLD
&& spineCurvature < CROUCH_ANGLE_THRESHOLD) {
return BehaviorTag::PROLONGED_CROUCHING; // 웅크림 지속
}
if (isGroomingPattern(recentFrames)) {
return BehaviorTag::EXCESSIVE_GROOMING; // 과도한 그루밍
}
if (avgMotion > HIGH_ACTIVITY_THRESHOLD) {
return BehaviorTag::HIGH_ACTIVITY; // 과활동
}
return BehaviorTag::NORMAL;
}
bool BehaviorDetector::isGroomingPattern(
const std::vector<OpticalFlowResult>& frames) {
// 머리 방향 반복 왕복 패턴 감지 (방향 벡터 주기성 분석)
// 0.5~2초 주기의 반복 동작 → 그루밍으로 판단
int reversalCount = 0;
for (size_t i = 1; i < frames.size(); ++i) {
// 연속 프레임 간 이동 방향 역전 횟수 카운트
if (frames[i].avgMagnitude > GROOM_MIN_MOTION
&& directionReversed(frames[i-1], frames[i])) {
++reversalCount;
}
}
return reversalCount >= GROOM_REVERSAL_MIN_COUNT;
}
4.4 Kotlin JNI 브릿지
// MotionAnalyzerJni.kt
class MotionAnalyzerJni {
init { System.loadLibrary("motion_analyzer") }
/**
* @param prevMatPtr OpenCV Mat 포인터 (이전 프레임, Grayscale)
* @param currMatPtr OpenCV Mat 포인터 (현재 프레임, Grayscale)
* @return FloatArray [평균이동량, 최대이동량, 유효특징점수, 행동태그코드]
*/
external fun computeOpticalFlow(prevMatPtr: Long, currMatPtr: Long): FloatArray
/**
* @param flowHistory 최근 N프레임 광학흐름 결과
* @param spineCurvature 자세 추정으로 계산된 척추 굴곡 각도
* @param gaitAsymmetry 보행 비대칭 점수
* @return Int 행동 태그 코드 (BehaviorTag enum ordinal)
*/
external fun classifyBehavior(
flowHistory: FloatArray,
spineCurvature: Float,
gaitAsymmetry: Float
): Int
}
5. 건강 점수 모델 (Health Scorer)
5.1 모델 개요
앞선 세 모델의 출력을 종합하여 최종 건강 점수(0~100)를 산출하는 경량 앙상블 모델.
입력 특징 벡터 (총 32차원)
| 특징 그룹 | 차원 | 설명 |
|---|---|---|
| 품종 보정값 | 2 | 품종별 평균 활동량, 평균 체형 지수 |
| 자세 특징 | 8 | 척추 굴곡, 보행 비대칭, 관절별 각도 (6개) |
| 움직임 특징 | 6 | 평균·최대·최소 이동량, 활동 빈도, 방향 엔트로피, 반복성 |
| 행동 태그 | 6 | 그루밍·절뚝거림·웅크림·과호흡·점프회피·과활동 (One-hot) |
| 시계열 요약 | 10 | 최근 7일 건강 점수 이동 평균, 추세 기울기, 이상 빈도 |
모델 구조: 경량 MLP (ONNX)
# health_scorer_model.py
import torch
import torch.nn as nn
class HealthScorer(nn.Module):
def __init__(self, input_dim: int = 32):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1),
nn.Sigmoid() # 출력: [0, 1] → ×100 = 건강 점수
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x) * 100.0 # [0, 100] 스케일
ONNX 변환
dummy_input = torch.randn(1, 32)
torch.onnx.export(
model, dummy_input,
"health_scorer.onnx",
input_names=["features"],
output_names=["health_score"],
dynamic_axes={"features": {0: "batch_size"}},
opset_version=17
)
5.2 학습 데이터 구성 전략
건강 점수 모델의 학습 데이터는 두 가지 경로로 구성한다.
경로 1: 규칙 기반 레이블 생성 (초기)
수의학 문헌 기반 행동-건강 규칙으로 초기 레이블을 생성한다.
def rule_based_label(features: dict) -> float:
score = 100.0
# 보행 이상: 최대 -30점
score -= min(features["gait_asymmetry"] * 60, 30)
# 저활동: 최대 -20점
if features["avg_motion"] < BREED_BASELINE_MOTION * 0.5:
score -= 20
# 과도한 그루밍: -10점
if features["grooming_flag"]:
score -= 10
# 웅크림 지속: -15점
if features["prolonged_crouching"]:
score -= 15
# 시계열 하락 추세: 최대 -10점
score -= min(abs(features["trend_slope"]) * 20, 10)
return max(score, 0.0)
경로 2: 수의사 레이블 데이터 (고도화)
앱 배포 후 수의사 협력을 통해 실제 진료 결과와 매칭된 레이블 데이터를 축적하여 모델을 재학습한다.
5.3 Android 통합 코드
// HealthScorer.kt
class HealthScorer @Inject constructor(
@ApplicationContext private val context: Context
) {
private val env: OrtEnvironment = OrtEnvironment.getEnvironment()
private val session: OrtSession by lazy {
val modelBytes = context.assets.open("health_scorer.onnx").readBytes()
env.createSession(
modelBytes,
OrtSession.SessionOptions().apply {
addNnapi() // NNAPI 가속 (API 27+)
setIntraOpNumThreads(2)
}
)
}
fun score(vector: HealthFeatureVector): Float {
val input = OnnxTensor.createTensor(
env,
arrayOf(vector.toFloatArray()), // shape: [1, 32]
longArrayOf (1, FEATURE_DIM)
)
val result = session.run (mapOf ("features" to input))
val output = (result[0]. value as Array<FloatArray>)[0][0]
input.close ()
result.close ()
return output.coerceIn (0 f, 100 f)
}
companion object {
const val FEATURE_DIM = 32 L
}
}
6. 모델 파일 관리
6.1 앱 내 모델 파일 구성
: ml-core/src/main/assets/
├── models/
│ ├── cat_breed_int 8. tflite (약 4 MB — INT 8 양자화)
│ ├── pose_landmarker_lite. task (약 3 MB — MediaPipe Lite)
│ └── health_scorer. onnx (약 0.1 MB — 경량 MLP)
└── model_manifest. json (버전 정보)
// model_manifest. json
{
"breed_classifier": {
"version": "1.0.0",
"filename": "cat_breed_int 8. tflite",
"num_classes": 67,
"input_size": 224,
"checksum": "sha 256:..."
},
"health_scorer": {
"version": "1.0.0",
"filename": "health_scorer. onnx",
"input_dim": 32,
"checksum": "sha 256:..."
}
}
6.2 모델 업데이트 흐름
앱 실행
│
▼
ModelRepository.checkUpdate ()
│ (백그라운드 WorkManager)
▼
서버 /api/models/latest 응답
│
├── 동일 버전 → 업데이트 없음
│
└── 신규 버전
│
▼
다운로드 → SHA 256 검증 → 내부 저장소 이동
│
▼
다음 앱 실행 시 새 모델 로드
7. 성능 벤치마크 목표
| 모델 | 디바이스 | 가속기 | 목표 시간 |
|---|---|---|---|
| BreedClassifier | 중급 (SD 778 G) | GPU Delegate | ≤ 150 ms |
| BreedClassifier | 고급 (SD 8 Gen 3) | GPU Delegate | ≤ 60 ms |
| PoseEstimator | 중급 | GPU Delegate | ≤ 40 ms/frame |
| PoseEstimator | 고급 | GPU Delegate | ≤ 20 ms/frame |
| MotionAnalyzer | 중급 | C++ NEON | ≤ 20 ms/frame |
| HealthScorer | 중급 | NNAPI | ≤ 10 ms |
| 전체 파이프라인 | 중급 | 복합 | ≤ 200 ms |
8. 향후 고도화 계획
| 단계 | 내용 | 예상 시점 |
|---|---|---|
| Phase 2 | 고양이 전용 Pose Landmarker 커스텀 학습 | 앱 배포 후 3개월 |
| Phase 2 | 흉부 호흡 주기 분석 (FFT 기반) 추가 | 앱 배포 후 3개월 |
| Phase 3 | 수의사 레이블 기반 HealthScorer 재학습 | 앱 배포 후 6개월 |
| Phase 3 | 야간 / 저조도 환경 대응 (ISP 전처리) | 앱 배포 후 6개월 |
| Phase 4 | 온디바이스 연합 학습 (Federated Learning) 검토 | 장기 과제 |
9. 관련 문서
| 문서 | 경로 |
|---|---|
| 전체 기획 개요 | 01_overview. md |
| Android 아키텍처 | 02_android_architecture. md |
| 백엔드 세부 설명 | 04_backend. md |
본 문서는 개발 진행에 따라 지속적으로 업데이트됩니다.
C
Contents
