Skip to main content

Pass NumPy Arrays to the Model

Pass NumPy Arrays to the Model — animated walkthrough overview

FieldValue
DifficultyIntermediate
Estimated Read Time10-15 minutes
Labelsnumpy, pytorch, tensor, io

If you are integrating Neat into an existing inference stack, this is the interop boundary you need: how host data becomes a Neat Tensor, and how a Neat Tensor becomes host data again. Getting it right up front prevents the classic integration bugs — wrong layout, silent dtype coercion, unexpected aliasing between the two worlds.

This is also where the two languages diverge most. Python users come from NumPy/PyTorch; C++ users come from OpenCV. The conversion concepts are identical, but the API names and types differ, so the per-language prose below matters. By the end you will have converted host data into a Neat tensor, inspected its payload without copying, and produced an owned copy that is safe to outlive the source buffer.

Walkthrough

Wrap host data as a tensor

The first move turns data you already hold into a Neat Tensor. You tag the image layout explicitly (RGB) so the runtime interprets the bytes correctly rather than guessing. copy=True (or the CPU memory choice in C++) decides whether the tensor owns its bytes or aliases the source — explicit ownership is the safe default when the source buffer may change or be freed.

simaai::neat::from_cv_mat(mat, ImageSpec::PixelFormat::RGB, TensorMemory::CPU) wraps a cv::Mat into a CPU-backed tensor.

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);

Inspect the payload

Once the data is a tensor, you can read it back. This is the round-trip half of interop: confirm shape and bytes survived the conversion before feeding anything downstream.

tensor.map_read() returns a Mapping exposing a raw data pointer and size_bytes. It is a view into the tensor's storage — no copy — which is why the example can checksum the leading bytes directly.

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();

Own a copy

Finally, produce data that is fully detached from the original source buffer — safe to keep after the input is gone. This is the copy you hand to long-lived consumers.

tensor.clone() copies into fresh CPU-owned storage, independent of the cv::Mat it came from.

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();

Run

Run the Python and C++ (prebuilt) commands from the Neat install root (the directory that contains share/ and lib/); run the build from source commands from the repo root. This chapter needs no model archive.

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

Expected output (C++):

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

Expected output (Python, with torch installed):

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

(Without torch, the Python build prints torch_roundtrip_skipped=True instead of the torch line.) To integrate this chapter's C++ source into your own project with a custom CMakeLists.txt (no extras folder required), see How to Run Tutorials on the landing page.

In Practice

The interop surface, summarized for quick reference once you move past the round-trip demo.

Conversion API

  • NumPy: pyneat.Tensor.from_numpy(array, copy=..., image_format=...) in; tensor.to_numpy(copy=...) out.
  • PyTorch: pyneat.Tensor.from_torch(tensor, copy=..., image_format=...) in; tensor.to_torch(copy=...) out.
  • OpenCV (C++): simaai::neat::from_cv_mat(mat, pixel_format, memory) in; tensor.map_read() for a zero-copy view; tensor.clone() for an owned copy.

Copy vs view

  • copy=True (Python) / clone() (C++) gives you data detached from the source — safe to keep after the source is freed or mutated.
  • copy=False / map_read() gives you a view that aliases the source. Cheaper, but only valid while the source stays alive and unchanged.

Layout and dtype

  • Always pass an explicit image_format / PixelFormat for image data so layout is interpreted, not guessed.
  • Neat does not silently coerce dtype — match the tensor dtype to the model's input contract before feeding it.

Full source

Show the complete C++ and Python programs
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];

// CORE LOGIC
// clone() copies into CPU-owned storage, detached from the cv::Mat buffer.
simaai::neat::Tensor owned = tensor.clone();

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;
}
}

Source