Search
castle

Cat Re-ID — 속도 최적화 및 C++ 포팅 가이드

전체 파이프라인 정상 동작 확인 후, 병목 구간 측정 기반의 단계별 최적화 수행 지침.

병목 측정 먼저

최적화 작업 시작 전 각 구간별 소요 시간을 정밀 측정하여 병목 위치를 파악하는 것이 필수적임. 일반적으로 추론부보다 이미지 전처리 또는 파일 I/O에서 병목이 발생하는 경우가 존재함.

# scripts/profile_pipeline.py
import time
import cv2
import numpy as np
from reid.container import build_detector, build_extractor, build_matcher
from reid.core.config import get_config
from reid.models import ReIdModel

cfg = get_config()
detector = build_detector(cfg)
extractor = build_extractor(cfg)
matcher = build_matcher(cfg)
pipeline = ReIdModel(detector, extractor, matcher, cfg=cfg)

# 대표 이미지 생성 (384x384, 3채널)
frame = np.zeros((384, 384, 3), dtype=np.uint8)

# 1. Detector 속도 측정
t0 = time.perf_counter()
detector_results = detector.predict(frame)
t1 = time.perf_counter()

# 2. Extractor 속도 측정
crops = [box.crop(frame) for box in detector_results.boxes] if detector_results else []
if crops:
    t2 = time.perf_counter()
    embeddings = extractor.predict_batch(crops)
    t3 = time.perf_counter()
else:
    t2 = t3 = time.perf_counter()

# 3. Matcher 속도 측정
if crops and len(embeddings) > 0:
    t4 = time.perf_counter()
    for emb in embeddings:
        match = matcher.match(emb)
    t5 = time.perf_counter()
else:
    t4 = t5 = time.perf_counter()

print(f"YOLO 탐지:       {(t1 - t0) * 1000:.1f} ms")
print(f"Embedding 추출:  {(t3 - t2) * 1000:.1f} ms")
print(f"KNN 매칭:        {(t5 - t4) * 1000:.1f} ms")

구간별 예상 소요 시간 (GPU 환경, MegaDescriptor-Large 기준)

구간 예상 시간 비고
YOLO 탐지 10~30 ms GPU 환경 최적화 상태
Embedding 추출 50~150 ms 주요 병목 구간
KNN 매칭 1~5 ms CPU 연산, 비중 낮음

Python 내 최적화 전략

경량 모델 선택

가장 직관적이고 효과가 큰 속도 개선 방식임.

모델 파라미터 크기 입력 사용 권장
MegaDescriptor-Tiny ~28 M ~110 MB 224 px 엣지/CPU 저전력 환경
MegaDescriptor-Small ~50 M ~200 MB 224 px 속도-정확도 밸런스
MegaDescriptor-Large ~229 M ~900 MB 384 px 고성능 서버 환경
# config.yaml 수정
model_name: "hf-hub:BVRA/MegaDescriptor-T-224"  # Tiny 모델 변경 시 3~5배 가속 효과
imgsz: 224

torch.no_grad()model.eval() 적용

추론 단계에서의 그래디언트 계산 그래프 생성을 비활성화하여 메모리 낭비 방지 및 속도를 향상시킴.

# reid/models/extractor/mega_descriptor/model.py 예시
class CombinedModel(nn.Module):
    def __init__(self, backbone, projection, has_custom_weights):
        super().__init__()
        self.backbone = backbone
        self.projection = projection
        self.has_custom_weights = has_custom_weights
    
    def forward(self, x):
        with torch.no_grad():  # 연산 그래프 생성을 중단하여 VRAM 및 추론 속도 최적화
            features = self.backbone(x)
        if self.has_custom_weights:
            return self.projection(features)
        return features

배치 추론 적용

동일 프레임 내 다중 고양이가 탐지되었을 때 순차 추론 대신 단일 배치 텐서로 묶어 연산 효율 극대화.

# reid/models/extractor/predict.py 배치 추론 예시
def predict_batch(self, imgs: list[np.ndarray]) -> np.ndarray:
    tensors = torch.stack([self.preprocess(img) for img in imgs]).to(self.device)
    with torch.no_grad():
        embeddings = self.model(tensors)
    return embeddings.cpu().numpy()

프레임 스킵 및 캐싱 기법

ByteTrack 추적 ID를 기반으로 하여 신규 ID 탐지 시에만 Re-ID 특징 추출을 수행하고, 기존 ID에 대해서는 식별 결과를 재사용함. (Re-ID 연산 횟수를 90% 이상 절감)

# config.yaml 설정 연동
track: True                  # 트래킹 활성화
candidate_interval: 10       # 미확정 개체 재매칭 주기 (Frame 단위)
lock_interval: 60            # 확정 개체 재검증 주기 (Frame 단위)

ONNX 변환으로 추론 가속

PyTorch 의존성 없이 C++ 런타임 및 경량화 환경 배포가 가능한 표준 포맷 변환 기법임.

가속 장점

  • 그래프 최적화: 레이어 노드 융합(Node Fusion), 상수 폴딩(Constant Folding) 등 적용
  • 런타임 효율: CPU 환경 20~30% 향상, GPU 환경 CUDA/TensorRT Provider 활용 시 2~5배 이상 가속 가능

ONNX 내보내기 구현

# reid/models/extractor/predict.py 내보내기 예시
def export(self, output_path: str) -> str:
    self.model.eval()
    dummy_input = torch.randn(1, 3, self.cfg.imgsz, self.cfg.imgsz).to(self.device)
    torch.onnx.export(
        self.model,
        dummy_input,
        output_path,
        input_names=["image"],
        output_names=["embedding"],
        dynamic_axes={"image": {0: "batch_size"}},
        opset_version=17
    )
    return output_path

TensorRT 가속 (GPU 환경)

NVIDIA GPU 하드웨어에 최적화된 엔진 빌드를 통해 최고의 FPS 성능 달성 가능.

# ONNX Runtime TensorRT Provider 연동 예시
import onnxruntime as ort

providers = [
    ("TensorrtExecutionProvider", {
        "trt_engine_cache_enable": True,
        "trt_engine_cache_path": "models/trt_cache/",
        "trt_fp16_enable": True
    }),
    "CUDAExecutionProvider",
    "CPUExecutionProvider"
]
session = ort.InferenceSession("weights/wildlife.onnx", providers=providers)

C++ 포팅 가이드

포팅 기대 효과 분석

구간 C++ 전환 기대치 사유
YOLO 추론 낮음 GPU 내부 CUDA 커널에서 실제 연산 수행
Re-ID 특징 추출 낮음 동일 수준의 GPU 가속 백엔드 호출
매칭 연산 (KNN) 매우 낮음 Python numpy/FAISS 라이브러리가 이미 C 구현체 기반
이미지 전처리/프레임 루프 보통 Python 인터프리터 루프 오버헤드 제거

포팅 구조 및 범위 정의

비즈니스 로직(학습, DB 가공)은 Python을 유지하고, 실시간 스트림 파이프라인 루프만 C++로 마이그레이션 수행.

cpp/
├── CMakeLists.txt
├── main.cpp                 # 프레임 캡처 및 파이프라인 루프 제어
├── detector.hpp/cpp         # YOLO ONNX Runtime C++ API 어댑터
├── embedder.hpp/cpp         # MegaDescriptor ONNX Runtime C++ API 어댑터
├── matcher.hpp/cpp          # Cosine Similarity 기반 KNN 매칭 알고리즘
└── overlay.hpp/cpp          # OpenCV C++ 활용 오버레이 드로잉

CMakeLists.txt 작성 예시

cmake_minimum_required(VERSION 3.18)
project(cat_reid)

set(CMAKE_CXX_STANDARD 17)

find_package(onnxruntime REQUIRED)
find_package(OpenCV REQUIRED)

add_executable(cat_reid
    main.cpp
    detector.cpp
    embedder.cpp
    matcher.cpp
    overlay.cpp
)

target_include_directories(cat_reid PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(cat_reid
    onnxruntime
    ${OpenCV_LIBS}
)

특징 추출기(Embedder) C++ 구현 예시

// cpp/embedder.hpp
#pragma once
#include <onnxruntime_cxx_api.h>
#include <opencv2/opencv.hpp>
#include <vector>

class Embedder {
public:
    explicit Embedder(const std::string& model_path);
    std::vector<float> extract(const cv::Mat& image);

private:
    Ort::Env env_;
    Ort::Session session_;
    std::string input_name_;
    cv::Mat preprocess(const cv::Mat& image);
};
// cpp/embedder.cpp
#include "embedder.hpp"

Embedder::Embedder(const std::string& model_path)
    : env_(ORT_LOGGING_LEVEL_WARNING, "embedder"),
      session_(env_, model_path.c_str(), Ort::SessionOptions{})
{
    Ort::AllocatorWithDefaultOptions alloc;
    input_name_ = session_.GetInputNameAllocated(0, alloc).get();
}

cv::Mat Embedder::preprocess(const cv::Mat& image) {
    cv::Mat resized, float_img;
    cv::resize(image, resized, cv::Size(224, 224));
    resized.convertTo(float_img, CV_32F, 1.0 / 255.0);
    float_img = (float_img - 0.5f) / 0.5f;

    // HWC -> CHW 변환
    cv::Mat chw;
    std::vector<cv::Mat> channels(3);
    cv::split(float_img, channels);
    cv::vconcat(channels, chw);
    return chw.reshape(1, {1, 3, 224, 224});
}

std::vector<float> Embedder::extract(const cv::Mat& image) {
    cv::Mat input = preprocess(image);
    std::vector<int64_t> input_shape = {1, 3, 224, 224};
    Ort::MemoryInfo mem_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    
    Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
        mem_info,
        reinterpret_cast<float*>(input.data),
        input.total(),
        input_shape.data(),
        input_shape.size()
    );

    const char* input_names[] = {input_name_.c_str()};
    const char* output_names[] = {"embedding"};
    
    auto output = session_.Run(
        Ort::RunOptions{nullptr},
        input_names, &input_tensor, 1,
        output_names, 1
    );

    float* data = output[0].GetTensorMutableData<float>();
    int64_t size = output[0].GetTensorTypeAndShapeInfo().GetElementCount();
    return std::vector<float>(data, data + size);
}

매칭기(Matcher) C++ 구현 예시

// cpp/matcher.hpp
#pragma once
#include <vector>
#include <string>

struct MatchResult {
    std::string cat_id;
    float similarity;
    bool is_known;
};

struct DbEntry {
    std::string cat_id;
    std::vector<float> embedding;
};

class KnnMatcher {
public:
    KnnMatcher(float threshold = 0.6f);
    void add(const std::string& cat_id, const std::vector<float>& embedding);
    MatchResult match(const std::vector<float>& query) const;

private:
    float threshold_;
    std::vector<DbEntry> db_;
    float cosine_similarity(const std::vector<float>& a, const std::vector<float>& b) const;
};
// cpp/matcher.cpp
#include "matcher.cpp"
#include <cmath>
#include <algorithm>

KnnMatcher::KnnMatcher(float threshold) : threshold_(threshold) {}

void KnnMatcher::add(const std::string& cat_id, const std::vector<float>& embedding) {
    db_.push_back({cat_id, embedding});
}

float KnnMatcher::cosine_similarity(const std::vector<float>& a, const std::vector<float>& b) const {
    float dot = 0.0f, norm_a = 0.0f, norm_b = 0.0f;
    for (size_t i = 0; i < a.size(); ++i) {
        dot += a[i] * b[i];
        norm_a += a[i] * a[i];
        norm_b += b[i] * b[i];
    }
    return dot / (std::sqrt(norm_a) * std::sqrt(norm_b) + 1e-8f);
}

MatchResult KnnMatcher::match(const std::vector<float>& query) const {
    float best_sim = -1.0f;
    std::string best_id = "Unknown";

    for (const auto& entry : db_) {
        float sim = cosine_similarity(query, entry.embedding);
        if (sim > best_sim) {
            best_sim = sim;
            best_id = entry.cat_id;
        }
    }

    bool is_known = best_sim >= threshold_;
    return {is_known ? best_id : "Unknown", best_sim, is_known};
}

최적화 전략 선택 가이드

시스템 가용 자원 및 요구 스펙에 대응하는 추천 최적화 단계 정의.

[1단계: 모델 경량화] -> [2단계: 추적/캐싱 기반 추론 생략] -> [3단계: ONNX 가속] -> [4단계: TRT 가속] -> [5단계: C++ 프레임 제어 루프 작성]

디바이스 구동 환경별 권장 셋업

환경 추천 모델 런타임 백엔드 목표 성능
CPU Only (PC/서버) MegaDescriptor-Tiny ONNX Runtime CPU 5~15 FPS
GPU (RTX 3060 이상) MegaDescriptor-Large PyTorch GPU / ONNX CUDA 30 FPS+
GPU 서버 (대형 망) MegaDescriptor-Large TensorRT GPU 60 FPS+
임베디드 엣지 (Jetson 등) MegaDescriptor-Tiny TensorRT C++ 15~30 FPS
left
right

C

Contents