본문으로 건너뛰기

NumPy 배열을 모델에 전달

필드
범주모델 및 추론
난이도중급
예상 소요 시간10-15 minutes
레이블numpy, pytorch, tensor, io

기존 추론 스택에 Neat을 통합하는 경우, 다음은 필요한 상호 운용 경계입니다. 호스트 데이터가 Neat Tensor로 변환되는 방식과 Neat Tensor가 다시 호스트 데이터로 변환되는 방식을 고려해야 합니다. 처음부터 제대로 처리하면 일반적인 통합 오류(잘못된 레이아웃, 암시적 dtype 변환, 두 영역 간의 예기치 않은 별칭)를 방지할 수 있습니다.

여기서 두 언어의 차이가 가장 두드러집니다. Python 사용자는 NumPy/PyTorch에서, C++ 사용자는 OpenCV에서 시작합니다. 변환 개념은 동일하지만 API 이름과 유형이 다르므로 아래의 언어별 설명이 중요합니다. 마지막에는 호스트 데이터를 Neat 텐서로 변환하고, 복사하지 않고도 페이로드를 검사하고, 소스 버퍼보다 오래 유지해도 안전한 소유된 복사본을 생성할 수 있습니다.

둘러보기

호스트 데이터를 텐서로 래핑

첫 번째 단계는 이미 보유하고 있는 데이터를 Neat Tensor로 변환하는 것입니다. 이미지 레이아웃을 명시적으로 지정(RGB)하여 런타임이 바이트를 추측하는 대신 올바르게 해석하도록 합니다. copy=True (또는 C++의 CPU 메모리 선택)는 텐서가 자체 바이트를 소유할지 또는 소스를 참조할지 결정합니다. 소스 버퍼가 변경되거나 해제될 수 있는 경우 명시적 소유권이 안전한 기본값입니다.

simaai::neat::from_cv_mat(mat, ImageSpec::PixelFormat::RGB, TensorMemory::CPU)cv::Mat을 CPU 기반 텐서로 래핑합니다.

tutorials/009_pass_numpy_to_model/pass_numpy_to_model.cpp
// from_cv_mat converts a cv::Mat into a CPU-backed Neat Tensor.
simaai::neat::Tensor tensor = simaai::neat::from_cv_mat(
rgb, simaai::neat::ImageSpec::PixelFormat::RGB, simaai::neat::TensorMemory::CPU);

페이로드 검사

데이터가 텐서가 되면 다시 읽을 수 있습니다. 이는 상호 운용의 절반에 해당합니다. 다운스트림으로 전달하기 전에 변환 중에 모양과 바이트가 손상되지 않았는지 확인합니다.

tensor.map_read()는 원시 data 포인터와 size_bytes를 노출하는 Mapping을 반환합니다. 이는 텐서의 저장소에 대한 이며 복사가 없으므로 예제에서 선행 바이트를 직접 체크섬할 수 있습니다.

tutorials/009_pass_numpy_to_model/pass_numpy_to_model.cpp
// map_read yields a Mapping with a raw pointer and size in bytes.
simaai::neat::Mapping mapped = tensor.map_read();

복사본 소유

마지막으로 원본 소스 버퍼와 완전히 분리된 데이터를 생성합니다. 이렇게 하면 입력이 제거된 후에도 안전하게 유지할 수 있습니다. 이것이 장기간 사용할 소비자가 사용할 복사본입니다.

tensor.clone()cv::Mat에서 가져온 것과 독립적으로 새로운 CPU 소유 저장소에 복사합니다.

tutorials/009_pass_numpy_to_model/pass_numpy_to_model.cpp
// clone() copies into CPU-owned storage, detached from the cv::Mat buffer.
simaai::neat::Tensor owned = tensor.clone();
if (owned.dense_bytes_tight() == 0U)
throw std::runtime_error("cloned tensor is empty");

실행

Neat 설치 루트(share/lib/가 포함된 디렉터리)에서 PythonC++(미리 빌드된 버전) 명령을 실행합니다. 소스에서 빌드 명령은 리포지토리 루트에서 실행합니다. 이 챕터에는 모델 아카이브가 필요하지 않습니다.

C++ (prebuilt):

./lib/sima-neat/tutorials/tutorial_009_pass_numpy_to_model \
--width 128 --height 96

C++ (build from source):

./build.sh --target tutorial_009_pass_numpy_to_model
./build/tutorials-standalone/tutorial_009_pass_numpy_to_model \
--width 128 --height 96

예상 출력(C++):

tensor_rank=3
tensor_bytes=36864
head_checksum=4342
clone_bytes=36864
[OK] 009_pass_numpy_to_model

예상 출력(Python, torch가 설치된 경우):

numpy_roundtrip_shape=(96, 128, 3)
torch_roundtrip_shape=(96, 128, 3)

(torch가 없는 경우 Python 빌드는 torch 라인 대신 torch_roundtrip_skipped=True를 출력합니다.) 사용자 지정 CMakeLists.txt를 사용하여 이 챕터의 C++ 소스를 자체 프로젝트에 통합하려면(추가 폴더가 필요하지 않음) 랜딩 페이지의 튜토리얼 실행 방법을 참조하십시오.

실전 활용

왕복 데모를 넘어서면 빠르게 참조할 수 있도록 요약된 상호 운용 인터페이스입니다.

변환 API

  • NumPy: pyneat.Tensor.from_numpy(array, copy=..., image_format=...) (입력); tensor.to_numpy(copy=...) (출력).
  • PyTorch: pyneat.Tensor.from_torch(tensor, copy=..., image_format=...) (입력); tensor.to_torch(copy=...) (출력).
  • OpenCV (C++): simaai::neat::from_cv_mat(mat, pixel_format, memory) (입력); 제로 복사 뷰를 위한 tensor.map_read(); 소유된 복사본을 위한 tensor.clone().

복사 vs 뷰

  • copy=True (Python) / clone() (C++)는 소스에서 분리된 데이터를 제공합니다. 소스가 해제되거나 변경된 후에도 안전하게 유지할 수 있습니다.
  • copy=False / map_read()는 소스를 참조하는 뷰를 제공합니다. 비용이 저렴하지만 소스가 유지되고 변경되지 않은 상태일 때만 유효합니다.

레이아웃 및 dtype

  • 이미지 데이터에 대해 항상 명시적인 image_format / PixelFormat를 전달하여 레이아웃이 해석되도록 하고 추측하지 않도록 합니다.
  • Neat는 dtype을 자동으로 변환하지 않습니다. 텐서의 dtype을 모델의 입력 계약과 일치시켜야 합니다.

전체 소스

전체 소스 프로그램 표시
tutorials/009_pass_numpy_to_model/pass_numpy_to_model.cpp
// Convert a cv::Mat into a Neat Tensor, map it read-only, and clone it.
//
// Usage:
// tutorial_009_pass_numpy_to_model [--width 128] [--height 96]

#include "neat.h"

#include <opencv2/core.hpp>

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>

namespace {

bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}

int parse_int_arg(int argc, char** argv, const std::string& key, int def) {
std::string value;
if (!get_arg(argc, argv, key, value))
return def;
return std::stoi(value);
}

} // namespace

int main(int argc, char** argv) {
try {
const int width = parse_int_arg(argc, argv, "--width", 128);
const int height = parse_int_arg(argc, argv, "--height", 96);

cv::Mat rgb(height, width, CV_8UC3, cv::Scalar(7, 17, 27));
if (!rgb.isContinuous())
rgb = rgb.clone();

// CORE LOGIC
// from_cv_mat converts a cv::Mat into a CPU-backed Neat Tensor.
simaai::neat::Tensor tensor = simaai::neat::from_cv_mat(
rgb, simaai::neat::ImageSpec::PixelFormat::RGB, simaai::neat::TensorMemory::CPU);

// map_read yields a Mapping with a raw pointer and size in bytes.
simaai::neat::Mapping mapped = tensor.map_read();

std::uint64_t checksum = 0;
const auto* bytes = static_cast<const std::uint8_t*>(mapped.data);
const std::size_t n = std::min<std::size_t>(mapped.size_bytes, 256);
for (std::size_t i = 0; i < n; ++i)
checksum += bytes[i];
if (tensor.shape.size() != 3U || mapped.size_bytes == 0U)
throw std::runtime_error("mapped tensor is empty or rank is wrong");

// CORE LOGIC
// clone() copies into CPU-owned storage, detached from the cv::Mat buffer.
simaai::neat::Tensor owned = tensor.clone();
if (owned.dense_bytes_tight() == 0U)
throw std::runtime_error("cloned tensor is empty");

std::cout << "tensor_rank=" << tensor.shape.size() << "\n";
std::cout << "tensor_bytes=" << mapped.size_bytes << "\n";
std::cout << "head_checksum=" << checksum << "\n";
std::cout << "clone_bytes=" << owned.dense_bytes_tight() << "\n";
std::cout << "[OK] 009_pass_numpy_to_model\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}

소스