Pass NumPy Arrays to the Model
| Field | Value |
|---|---|
| Difficulty | Intermediate |
| Estimated Read Time | 10-15 minutes |
| Labels | numpy, 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.
// 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.
// 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.
// 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/PixelFormatfor 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
// 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;
}
}