객체 감지
이 튜토리얼에서는 RBLN SDK C/C++ Runtime API를 사용하여 PyTorch YOLOv8 모델을 배포하는 방법을 배우게 됩니다. 모델은 RBLN SDK Python API를 사용하여 컴파일되며, 그 결과로 생성된 *.rbln 파일은 RBLN SDK C/C++ Runtime API를 사용하여 배포됩니다.
이러한 접근 방식은 Python API를 사용한 간편한 모델 준비와 C/C++의 빠른 추론 성능을 결합합니다.
튜토리얼에서 사용된 전체 코드는 RBLN Model Zoo에서 확인할 수 있습니다.
이 튜토리얼은 다음 단계들을 포함합니다:
- Python API를 사용하여
YOLOv8m모델을 컴파일하고 로컬 저장소에 저장하는 방법
- 컴파일된 모델을 C/C++ 런타임 기반 추론 환경에서 배포하는 방법
전제 조건
시작하기 전에, 다음 패키지들이 설치되어 있는지 확인해 주세요:
- 모델 컴파일
- RBLN SDK C/C++ Runtime API
1단계. 컴파일 방법
RBLN Python API는 RBLN SDK 내에서 컴파일과 추론을 모두 처리할 수 있는 포괄적인 기능을 제공하지만, RBLN SDK C/C++ Runtime API는 추론 작업만 지원합니다. 따라서 본 튜토리얼에서는 모델 컴파일에는 RBLN Python API를 사용하고, 추론 작업에는 RBLN SDK C/C++ Runtime API를 사용합니다.
모델 준비
먼저 ultralytics에서 Yolov8m 모델을 가져올 수 있습니다.
| from ultralytics import YOLO
import rebel
import torch
model_name = "yolov8m"
yolo = YOLO(f"{model_name}.pt")
model = yolo.model.eval()
model(torch.zeros(1, 3, 640, 640))
|
모델 컴파일
torch 모델 torch.nn.Module이 준비되면, rebel.compile_from_torch() 메서드를 사용하여 간단히 컴파일할 수 있습니다.
NPU가 호스트 머신에 설치되어 있다면, rebel.compile_from_torch() 함수에서 npu 인자를 생략할 수 있습니다. 이 경우 함수가 자동으로 설치된 NPU를 감지하고 사용합니다. 그러나 NPU가 호스트 머신에 설치되어 있지 않다면, 오류를 방지하기 위해 npu 인자를 사용하여 대상 NPU를 지정해야 합니다.
현재 지원되는 NPU 이름은 RBLN-CA02, RBLN-CA12 두 가지입니다. 대상 NPU의 이름을 모르는 경우, NPU가 설치된 호스트 머신의 명령어 입력창에서 rbln-stat 명령을 실행하여 확인할 수 있습니다.
컴파일된 모델을 로컬 저장소에 저장하려면, compiled_model.save() 메서드를 사용할 수 있습니다.
| # 컴파일 모델
compiled_model = rebel.compile_from_torch(model, [("x", [1, 3, 640, 640], "float32")], npu="RBLN-CA02")
# 로컬 저장소에 컴파일된 모델을 저장
compiled_model.save(f"{model_name}.rbln") # model_name = yolov8m
|
컴파일 진행
앞서 설명된 컴파일 코드는 compile.py에 포함되어 있습니다. 모델을 컴파일하고 rbln 파일을 생성하려면 다음 명령으로 compile.py를 실행하세요:
| $ python3 compile.py --model-name=yolov8m
|
이 과정이 성공적으로 완료하면, 로컬 저장소에서 yolov8m.rbln을 찾을 수 있습니다. 이 파일은 컴파일된 YOLOv8m 모델을 포함하고 있으며, RBLN SDK C/C++ Runtime API를 사용하여 배포할 준비가 된 상태입니다.
2단계. RBLN SDK C/C++ Runtime API를 사용한 배포 방법
이제 RBLN SDK C/C++ Runtime API를 사용하여 컴파일된 모델을 로드하고, 추론을 실행하고, 출력 결과를 확인할 수 있습니다.
CMake 빌드 스크립트 준비
이 튜토리얼은 이미지 전/후처리를 위해 OpenCV를 사용하고, 명령줄 인터페이스(CLI)에서 사용자 매개변수를 파싱하기 위해 argparse를 사용합니다.
다음 CMake 스크립트는 외부 패키지에 대한 의존성과 이들을 예제 애플리케이션 코드와 연결하는 방법을 설명합니다.
| # 외부 패키지에 대한 의존성 정의
include(FetchContent)
include(cmake/opencv.cmake)
include(cmake/argparse.cmake)
# 실행 파일의 이름 정의
add_executable(object_detection main.cc)
# 패키지 의존성에 대한 링크 정보 업데이트: OpenCV
find_package(OpenCV CONFIG REQUIRED)
target_link_libraries(object_detection ${OpenCV_LIBS})
# 의존성에 대한 링크 정보 업데이트: RBLN
find_package(rbln CONFIG REQUIRED)
target_link_libraries(object_detection rbln::rbln_runtime)
# 의존성 포함 업데이트: argparse
target_include_directories(object_detection PRIVATE ${argparse_INCLUDE_DIRS})
|
입력 준비
사전 훈련된 Yolov8m 모델에 필요한 전처리된 이미지를 입력 데이터로 준비해야 합니다. OpenCV가 제공하는 다양한 비전 API를 사용하여 입력 이미지에 대한 전처리를 수행합니다.
| // 이미지 전처리
std::string input_path = "${SAMPLE_PATH}/people4.jpg";
cv::Mat image;
try {
image = cv::imread(input_path);
} catch (const cv::Exception &err) {
std::cerr << err.what() << std::endl;
std::exit(1);
}
// CV 행렬로 이미지 변환
cv::Mat blob =
cv::dnn::blobFromImage(GetSquareImage(image, 640), 1. / 255., cv::Size(),
cv::Scalar(), true, false, CV_32F);
|
추론 실행
RBLN SDK C/C++ Runtime API는 동기와 비동기 추론 방식을 지원합니다. 간략화된 API들에 대한 설명은 아래를 참조하세요.
RBLN API rbln_create_model은 저장된 모델의 경로를 입력 인자로 전달하여 컴파일된 모델을 로드하는 데 사용됩니다.
또한, rbln_create_runtime을 사용하여 RBLNModel, 모듈 이름, 장치 ID로부터 동기 런타임을 생성할 수 있습니다.
비동기 동작을 위해서는 동기식 런타임에 사용한 것과 동일한 인자들을 rbln_create_async_runtime에 전달하여 비동기 런타임을 생성할 수 있습니다.
런타임에 입력 이미지를 할당하기 위해 rbln_set_input을 사용합니다. 이 API는 RBLNRuntime, 입력 버퍼의 인덱스, 전처리된 버퍼의 주소를 인자로 받습니다. 해당 API는 동기식 동작에서만 해당됩니다.
모든 입력이 업데이트되면, RBLNRuntime을 인자로 rbln_run을 호출하여 동기 추론을 수행할 수 있습니다.
비동기 동작의 경우 rbln_async_run에 입력 버퍼와 출력 버퍼를 전달함으로써 비동기 추론이 수행 가능합니다.
마지막으로, rbln_get_output을 사용하여 추론 결과가 포함된 출력 버퍼를 검색할 수 있습니다. 이 API는 RBLNRuntime과 출력 인덱스를 인자로 받습니다. 비동기식 동작의 경우, rbln_run을 호출할 때 입력 버퍼와 출력 버퍼를 전달했으므로, 해당 출력 버퍼들을 직접 참조하면 됩니다.
각 추론 모드에서 필요한 API 사용법에 대해서는 다음 두 가지 예제를 참조해 주세요:
-
동기 실행
| std::string model_path = "${SAMPLE_PATH}/yolov8m.rbln";
RBLNModel *mod = rbln_create_model(model_path.c_str());
RBLNRuntime *rt = rbln_create_runtime(mod, "default", 0, 0);
// 입력 데이터 설정
rbln_set_input(rt, 0, blob.data);
// 동기 추론 실행
rbln_run(rt);
// 출력 결과 가져오기
void *data = rbln_get_output(rt, 0);
|
-
비동기 실행
| std::string model_path = "${SAMPLE_PATH}/yolov8m.rbln";
RBLNModel *mod = rbln_create_model(model_path.c_str());
RBLNRuntime *rt = rbln_create_async_runtime(mod, "default", 0, 0);
// 출력 버퍼 할당
auto buf_size = rbln_get_layout_nbytes(rbln_get_output_layout(rt, 0));
std::vector<float> logits(buf_size/sizeof(float));
// 비동기 추론 실행
int rid = rbln_async_run(rt, blob.data, logits.data());
// 추론 완료 대기
rbln_async_wait(rt, rid, 1000);
|
후처리
출력 data는 (1, 84, 8400) 형태의 float32 데이터 배열로, 각 요소는 검출된 객체의 좌표, 클래스 ID, 그리고 신뢰도 점수를 나타냅니다.
이 출력은 처리 과정에서 박스, 신뢰도, 클래스 ID으로 분리됩니다. 그 다음, 비최대값 억제(NMS) 과정을 진행합니다. NMS가 완료된 후, 검출 결과는 박스 출력을 위한 후처리 과정을 거칩니다.
아래의 샘플 코드는 다음 순서로 진행됩니다:
- 모델 출력 데이터를 OpenCV Mat 형식으로 변환합니다.
- 검출된 객체들에 대해 반복:
- 경계 박스 좌표를 계산합니다.
- 클래스 신뢰도 점수를 찾고 가장 높은 점수의 클래스를 선택합니다.
- 이 정보를 배열에 저장합니다.
- 중복 검출 정보를 제거하기 위해 NMS를 적용합니다.
- NMS 후 남은 각 검출에 대해:
- 원본 이미지에 경계 박스를 그립니다.
- 해당 카테고리에서 일치하는 클래스 이름과 신뢰도 점수를 박스 위에 텍스트로 추가합니다.
- 결과 이미지를 로컬 저장소에 저장합니다.
| // NMS를 위한 후처리
const RBLNTensorLayout *layout = rbln_get_output_layout(rt, 0);
cv::Mat logits{layout->ndim, layout->shape, CV_32F};
memcpy(logits.data, data, rbln_get_layout_nbytes(layout));
std::vector<cv::Rect> nms_boxes;
std::vector<float> nms_confidences;
std::vector<size_t> nms_class_ids;
for (size_t i = 0; i < layout->shape[2]; i++) {
auto cx = logits.at<float>(0, 0, i);
auto cy = logits.at<float>(0, 1, i);
auto w = logits.at<float>(0, 2, i);
auto h = logits.at<float>(0, 3, i);
auto x = cx - w / 2;
auto y = cy - h / 2;
cv::Rect rect{static_cast<int>(x), static_cast<int>(y), static_cast<int>(w),
static_cast<int>(h)};
float confidence = std::numeric_limits<float>::min();
int cls_id;
for (size_t j = 4; j < layout->shape[1]; j++) {
if (confidence < logits.at<float>(0, j, i)) {
confidence = logits.at<float>(0, j, i);
cls_id = j - 4;
}
}
nms_boxes.push_back(rect);
nms_confidences.push_back(confidence);
nms_class_ids.push_back(cls_id);
}
std::vector<int> nms_indices;
cv::dnn::NMSBoxes(nms_boxes, nms_confidences, 0.25f, 0.45f, nms_indices);
// Draw output image
cv::Mat output_img = image.clone();
for (size_t i = 0; i < nms_indices.size(); i++) {
auto idx = nms_indices[i];
auto class_id = nms_class_ids[idx];
auto scaled_box = ScaleBox(nms_boxes[idx], output_img.size(), 640);
cv::rectangle(output_img, scaled_box, cv::Scalar(255, 0, 0));
std::stringstream ss;
ss << COCO_CATEGORIES[class_id] << ": " << nms_confidences[idx];
cv::putText(output_img, ss.str(), scaled_box.tl() - cv::Point(0, 1),
cv::FONT_HERSHEY_DUPLEX, 1, cv::Scalar(255, 0, 0));
}
cv::imwrite("result.jpg", output_img);
|
리소스 해제
| // 런타임 해제
rbln_destroy_runtime(rt);
// 모델 해제
rbln_destroy_model(mod);
|
CMake를 사용하여 빌드하는 방법
위의 API 예제들에 대한 전체 코드는 RBLN Model Zoo C++ 예제에 포함되어 있습니다. 다음 명령어를 사용하여 코드를 쉽게 컴파일하고 실행 가능한 바이너리를 생성할 수 있습니다:
${SAMPLE_PATH}는 예제 애플리케이션의 경로를 나타냅니다. (예: rbln-model-zoo/cpp/object_detection)
| mkdir ${SAMPLE_PATH}/build
cd ${SAMPLE_PATH}/build
cmake ..
make
|
Note
앞서 언급했듯이, 예제 애플리케이션은 이미지 처리 작업을 위해 OpenCV API를 사용합니다. 이를 위해, CMake 빌드 시스템이 OpenCV를 소스에서 직접 가져와 설치합니다. 이 과정은 시스템 사양과 인터넷 연결 속도에 따라 5분 이상 소요될 수 있습니다.
실행 파일 실행 방법
위의 모든 단계를 완료했다면, cmake 디렉토리 아래에서 동기식과 비동기식 추론을 위한 object_detection과 object_detectionn_async라는 이름의 실행 가능한 바이너리들을 찾을 수 있습니다.
- 동기 실행
| ${SAMPLE_PATH}/build/object_detection -i ${SAMPLE_PATH}/people4.jpg -m ${SAMPLE_PATH}/yolov8m.rbln
|
- 비동기 실행
| ${SAMPLE_PATH}/build/object_detection_async -i ${SAMPLE_PATH}/people4.jpg -m ${SAMPLE_PATH}/yolov8m.rbln
|
출력 결과는 다음과 같습니다: