Creating ImageTensors
Image data needs to be converted into an ImageTensor before it can be published into a pipeline. An ImageTensor can be created from a file or from a memory buffer.
Canonical inference format
The internal format of ImageTensors in the DENKflow library is this:
- numeric type:
float32 - numeric range:
[0, 1] - memory layout: Contiguous C-order
BCHW - channel count: 3
- channel order:
BGR
Where the dimensions stand for:
B: Batch sizeC: ChannelsH: HeightW: Width
The file-based and generic in-memory constructors handle the conversion to this format for you. The pre-normalized in-memory constructors require the input to already match it.
Loading from a file
DENKflow loads and normalizes the image from a file, so no extra metadata arguments are required.
- Python
- C / C++
ImageTensor.from_file(path)
- Input: path to one image file
- Normalization: handled automatically
- Result shape:
[1, 3, H, W]
image_tensor = denkflow.ImageTensor.from_file("image.jpg")
ImageTensor.from_files(paths)
Batch from multiple image files. All images must have the same size.
- Result shape:
[B, 3, H, W]
image_tensor = denkflow.ImageTensor.from_files(["a.jpg", "b.jpg"])
denkflow_image_tensor_from_file(...)
- Input: one file path
- Normalization: handled automatically
- Result shape:
[1, 3, H, W]
DenkflowImageTensor* image_tensor = NULL;
handle_error(
denkflow_image_tensor_from_file(&image_tensor, "image.jpg"),
"denkflow_image_tensor_from_file"
);
denkflow_image_tensor_from_files(...)
Batch from multiple image files. All images must have the same size.
- Result shape:
[B, 3, H, W]
const char* paths[] = {"a.jpg", "b.jpg"};
DenkflowImageTensor* image_tensor = NULL;
handle_error(
denkflow_image_tensor_from_files(&image_tensor, paths, 2),
"denkflow_image_tensor_from_files"
);
Loading from in-memory data
Use these when the image data is already in memory — for example coming from a frame grabber, a camera SDK, an image decoded with OpenCV, or a region-of-interest cropped from another buffer.
There are three constructors that all return an ImageTensor for publishing into a pipeline:
| Variant | Python | C / C++ | Copies data? | Converts data? |
|---|---|---|---|---|
| Generic | from_numpy | denkflow_image_tensor_from_buffer | Yes | Yes |
| Pre-normalized with Copy | from_numpy_raw | denkflow_image_tensor_from_buffer_raw | Yes | No |
| Pre-normalized no Copy | from_numpy_unsafe | denkflow_image_tensor_from_buffer_unsafe | No | No |
The generic variant is the default choice: the buffer / ndarray has
an arbitrary data type, channel order and memory layout (e.g. uint8 BGR
HWC from cv2.imread, or a uint16 HSI camera frame), and DENKflow
performs the conversion to the canonical inference format internally. When
using the C-API, the memory layout must always be contiguous and C-order,
regardless which of the 3 functions is used.
The two pre-normalized variants skip all conversions, so the input must
already be in the canonical inference format.
Use the _raw flavour when you want to free or reuse the buffer immediately
after the call, and the _unsafe flavour only when the additional copy is
a measurable bottleneck and you can guarantee that the buffer outlives the
pipeline run that consumes the tensor.
Function signatures
- Python
- C / C++
class ImageTensor:
@staticmethod
def from_numpy(
array: numpy.ndarray,
memory_layout: str,
channel_layout: str,
) -> ImageTensor: ...
@staticmethod
def from_numpy_raw(array: numpy.ndarray) -> ImageTensor: ...
@staticmethod
def from_numpy_unsafe(array: numpy.ndarray) -> ImageTensor: ...
The data type for from_numpy is inferred from array.dtype. The two
pre-normalized variants require array.dtype == numpy.float32.
// Generic constructor with on-the-fly conversion
DenkflowResult denkflow_image_tensor_from_buffer(
DenkflowImageTensor** image_tensor,
const void* buffer,
const char* data_type, // e.g. "uint8", "float32", "u16"
size_t image_batch_size,
size_t image_width,
size_t image_height,
size_t image_channels,
const char* memory_layout, // e.g. "HWC", "BHWC", "BCHW"
const char* channel_layout // e.g. "BGR", "RGB", "RGBA", "GRAY"
);
// Pre-normalized BGR BCHW float32, copied internally
DenkflowResult denkflow_image_tensor_from_buffer_raw(
DenkflowImageTensor** image_tensor,
const float* buffer,
size_t image_batch_size,
size_t image_channels,
size_t image_height,
size_t image_width
);
// Pre-normalized BGR BCHW float32, NO copy — caller manages the lifetime
DenkflowResult denkflow_image_tensor_from_buffer_unsafe(
DenkflowImageTensor** image_tensor,
const float* buffer,
size_t image_batch_size,
size_t image_channels,
size_t image_height,
size_t image_width
);
The data type for denkflow_image_tensor_from_buffer is selected via the
data_type string parameter. The two pre-normalized variants always expect
a float* buffer.
Notes
These rules apply identically to the Python and C/C++ APIs.
- The
data_type(C/C++) /array.dtype(Python) for the generic constructor must be one of:uint8,uint16,uint32,uint64,int8,int16,int32,int64,float32,float64,bool. The C short forms (u8,i16,f32, …) are also accepted. (Packed bool arrays are not supported.) memory_layoutis any combination of the dimension lettersB(batch),C(channel),H(height) andW(width). It must contain exactly oneHand oneW, at most oneBand at most oneC. IfBorCare omitted, they are assumed to be 1. The parameter is case-insensitive.channel_layoutis"gray"/"grayscale"(1 channel) or any 3- or 4-letter permutation ofR,G,Band (optionally)A, e.g."BGR","RGB","BGRA","RGBA". The parameter is case-insensitive.- Common combinations:
- Image read via OpenCV (
cv2.imread/cv::Mat::data):memory_layout="HWC",channel_layout="BGR". - Image read via PIL (
Image.open(...).convert("RGB")thennp.asarray):memory_layout="HWC",channel_layout="RGB".
- Image read via OpenCV (
- The pre-normalized constructors (
from_numpy_raw,from_numpy_unsafe,denkflow_image_tensor_from_buffer_raw,denkflow_image_tensor_from_buffer_unsafe) perform no conversions or sanity checks on the array contents. These functions are not guaranteed to throw an error. Passing data that is not normalized produces incorrect inference results. The following is required of the input:- The data type must be 32-bit
float(Python:numpy.float32) - All values must be within the range
[0, 1] - Memory layout must be
BCHW - Memory layout must be contiguous
- Must have 3 channels
- Channel order must be
BGR
- The data type must be 32-bit
- The
_unsafevariants do not copy the input buffer; they borrow it. You must keep the buffer alive (and untouched by other writers) until the pipeline run that consumes the tensor has returned. Violating this invariant is undefined behaviour.
Example: All three constructors side by side
- Python
- C / C++
import numpy as np
import denkflow
# Common image dimensions for this example.
batch, channels, height, width = 1, 3, 480, 640
# ----------------------------------------------------------------------
# 1) ImageTensor.from_numpy
# Typical case: a uint8 BGR array with HWC layout, e.g. the output
# of `cv2.imread`. DENKflow converts the dtype (uint8 → float32 in
# [0, 1]), reorders the channels (BGR is already correct here) and
# transposes HWC → BCHW.
# ----------------------------------------------------------------------
# In practice this would be `cv2.imread("path/to/image.jpg")`:
opencv_like_array = np.zeros((height, width, channels), dtype=np.uint8)
tensor_from_numpy = denkflow.ImageTensor.from_numpy(
opencv_like_array,
memory_layout="HWC", # "HWC" with an implicit batch is fine; "BHWC" would be equivalent
channel_layout="BGR",
)
# `tensor_from_numpy` is now ready to be published into a pipeline.
# ----------------------------------------------------------------------
# 2) ImageTensor.from_numpy_raw
# Your data is already a normalized float32 BGR BCHW array (e.g.
# produced by your own preprocessing pipeline). DENKflow skips all
# conversions but still copies the data so you can free / reuse the
# array immediately after the call.
# ----------------------------------------------------------------------
float_bchw_array = np.zeros((batch, channels, height, width), dtype=np.float32)
# ... fill `float_bchw_array` with values in [0, 1], in BGR BCHW order ...
tensor_from_raw = denkflow.ImageTensor.from_numpy_raw(float_bchw_array)
# `float_bchw_array` may be freed or reused now.
# ----------------------------------------------------------------------
# 3) ImageTensor.from_numpy_unsafe
# Same input requirements as `from_numpy_raw`, but DENKflow does NOT
# copy the data — it borrows the underlying buffer. You MUST keep
# the array alive (and untouched) until the pipeline run that
# consumes the tensor has finished.
# ----------------------------------------------------------------------
persistent_float_array = np.zeros((batch, channels, height, width), dtype=np.float32)
# ... fill with values in [0, 1], in BGR BCHW order ...
tensor_from_unsafe = denkflow.ImageTensor.from_numpy_unsafe(persistent_float_array)
# IMPORTANT: do NOT modify or let `persistent_float_array` go out of scope
# until the pipeline run that consumes `tensor_from_unsafe` has returned.
# From here on, all three tensors can be used identically:
# pipeline.publish_image_tensor(input_topic, tensor)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "denkflow.h"
void handle_error(enum DenkflowResult error_code, const char* function_name) {
printf("%s: %d", function_name, (int32_t)error_code);
if (error_code != DenkflowResult_Ok) {
char* error_buffer = (char*)malloc(DENKFLOW_ERROR_BUFFER_SIZE);
denkflow_get_last_error(error_buffer);
printf(" (%s)\n", error_buffer);
free(error_buffer);
exit(EXIT_FAILURE);
}
printf("\n");
}
int main() {
enum DenkflowResult r;
// Common image dimensions for this example.
const size_t batch = 1;
const size_t channels = 3;
const size_t height = 480;
const size_t width = 640;
// ---------------------------------------------------------------
// 1) denkflow_image_tensor_from_buffer
// Typical case: a uint8 BGR buffer with HWC layout, e.g. from
// OpenCV's cv::Mat::data. DENKflow converts the data type
// (uint8 → float32 in [0, 1]), reorders the channels (BGR is
// already correct here) and transposes HWC → BCHW.
// ---------------------------------------------------------------
uint8_t* opencv_like_buffer = (uint8_t*)calloc(batch * height * width * channels, sizeof(uint8_t));
// ... fill `opencv_like_buffer` from your camera / file / frame grabber ...
DenkflowImageTensor* tensor_from_buffer = NULL;
r = denkflow_image_tensor_from_buffer(
&tensor_from_buffer,
opencv_like_buffer,
"uint8",
batch,
width, // NOTE: width comes BEFORE height in this signature
height,
channels,
"BHWC", // batch, height, width, channels — "HWC" with an implicit batch is also fine
"BGR"
);
handle_error(r, "denkflow_image_tensor_from_buffer");
// `tensor_from_buffer` is now ready to be published into a pipeline.
// ---------------------------------------------------------------
// 2) denkflow_image_tensor_from_buffer_raw
// Your data is already a normalized float32 BGR BCHW buffer
// (e.g. produced by your own preprocessing pipeline). DENKflow
// skips all conversions but still copies the data so you can
// free your buffer immediately after the call.
// ---------------------------------------------------------------
float* float_bchw_buffer = (float*)calloc(batch * channels * height * width, sizeof(float));
// ... fill with values in [0, 1], in BGR BCHW order ...
DenkflowImageTensor* tensor_from_raw = NULL;
r = denkflow_image_tensor_from_buffer_raw(
&tensor_from_raw,
float_bchw_buffer,
batch,
channels,
height,
width
);
handle_error(r, "denkflow_image_tensor_from_buffer_raw");
// `float_bchw_buffer` may be freed or reused now.
// ---------------------------------------------------------------
// 3) denkflow_image_tensor_from_buffer_unsafe
// Same input requirements as `_raw`, but DENKflow does NOT
// copy the data — it borrows your buffer. You MUST keep the
// buffer alive (and untouched) until the pipeline run that
// consumes the tensor has finished.
// ---------------------------------------------------------------
float* persistent_float_buffer = (float*)calloc(batch * channels * height * width, sizeof(float));
// ... fill with values in [0, 1], in BGR BCHW order ...
DenkflowImageTensor* tensor_from_unsafe = NULL;
r = denkflow_image_tensor_from_buffer_unsafe(
&tensor_from_unsafe,
persistent_float_buffer,
batch,
channels,
height,
width
);
handle_error(r, "denkflow_image_tensor_from_buffer_unsafe");
// IMPORTANT: do NOT free or modify `persistent_float_buffer` until the
// pipeline run that consumes `tensor_from_unsafe` has returned.
// From here on, all three tensors can be used identically:
// denkflow_initialized_pipeline_publish_image_tensor(...)
// (which transfers ownership of the tensor pointer).
free(opencv_like_buffer);
free(float_bchw_buffer);
free(persistent_float_buffer); // free only after the pipeline run that consumed `tensor_from_unsafe` has returned
return 0;
}
Publishing into a pipeline
Once you have an ImageTensor, publish it into the pipeline's input topic.
Publishing transfers ownership of the tensor — it cannot be used afterwards.
- Python
- C / C++
pipeline.publish_image_tensor("camera/image", image_tensor)
handle_error(
denkflow_initialized_pipeline_publish_image_tensor(
initialized_pipeline,
"camera/image",
&image_tensor
),
"denkflow_initialized_pipeline_publish_image_tensor"
);
Which constructor should you pick?
- For input you have on disk, use
from_file/denkflow_image_tensor_from_file(or the_filesplural variants for same-sized batches). - For in-memory data, default to the generic in-memory constructor
(
from_numpy/FromImageData/denkflow_image_tensor_from_buffer). The conversion cost is small relative to model inference and it eliminates an entire class of "wrong color order" / "wrong dtype" / "wrong layout" bugs. - Switch to
from_numpy_raw/denkflow_image_tensor_from_buffer_rawonly after you've already produced canonicalBGR BCHW float32data yourself and want to drop the redundant conversion step. - Switch to
from_numpy_unsafe/denkflow_image_tensor_from_buffer_unsafeonly when the extra copy performed by the_rawflavour is a measurable bottleneck and you can guarantee that the buffer stays alive and untouched until the pipeline run consuming the tensor has returned.