dart_tensor_preprocessing
Tensor preprocessing library for Flutter/Dart. NumPy-like transforms pipeline for ONNX Runtime, TFLite, and other AI inference engines.
Features
- PyTorch Compatible: Matches PyTorch/torchvision tensor operations
- Non-blocking: Isolate-based async execution prevents UI jank
- Type-safe: ONNX-compatible tensor types (Float32, Int64, Uint8, etc.)
- Zero-copy: View/stride manipulation for reshape/transpose operations
- Declarative: Chain operations into reusable pipelines
- SIMD Accelerated: Float32/Float64 vectorized operations for 2-4x speedup
- Memory Efficient: Buffer pooling, uninitialized allocation, fused operations
Installation
dependencies:
dart_tensor_preprocessing: ^1.0.0
Quick Start
import 'dart:typed_data';
import 'package:dart_tensor_preprocessing/dart_tensor_preprocessing.dart';
// Create a tensor from image data (HWC format, Uint8)
final imageData = Uint8List.fromList([/* RGB pixel data */]);
final tensor = TensorBuffer.fromUint8List(imageData, [height, width, 3]);
// Use a preset pipeline for ImageNet models
final pipeline = PipelinePresets.imagenetClassification();
final result = await pipeline.runAsync(tensor);
// result.shape: [1, 3, 224, 224] (NCHW, Float32, normalized)
Pipeline Presets
| Preset | Output Shape | Use Case |
|---|---|---|
imagenetClassification() |
1, 3, 224, 224 |
ResNet, VGG, etc. |
objectDetection() |
1, 3, 640, 640 |
YOLO, SSD |
faceRecognition() |
1, 3, 112, 112 |
ArcFace, FaceNet |
clip() |
1, 3, 224, 224 |
CLIP models |
mobileNet() |
1, 3, 224, 224 |
MobileNet family |
Custom Pipeline
final pipeline = TensorPipeline([
ToTensorOp(normalize: true), // HWC -> CHW, scale to [0,1]
ResizeOp(height: 224, width: 224, antialias: true),
NormalizeOp.imagenet(), // ImageNet mean/std
UnsqueezeOp.batch(), // Add batch dimension
]);
// Sync execution
final result = pipeline.run(input);
// Async execution (runs in isolate)
final result = await pipeline.runAsync(input);
// Async with custom isolate threshold (default: 100,000 elements)
// Small tensors skip isolate overhead and run synchronously
final result = await pipeline.runAsync(input, isolateThreshold: 50000);
Available Operations
Resize & Crop
ResizeOp- Resize to fixed dimensions (nearest, bilinear, bicubic, area, lanczos) with ONNX-compatible coordinate transform modesResizeShortestOp- Resize preserving aspect ratioCenterCropOp- Center crop to fixed dimensionsClipOp- Element-wise value clamping (presets: unit, symmetric, uint8)PadOp- Padding with multiple modes (constant, reflect, replicate, circular)SliceOp- Python-like tensor slicing with negative index support
Normalization
NormalizeOp- Channel-wise normalization (presets: ImageNet, CIFAR-10, symmetric)ScaleOp- Scale values (e.g.,0-255to0-1)BatchNormOp- Batch normalization for CNN inference (PyTorch compatible)LayerNormOp- Layer normalization for Transformer inference (presets: BERT, BERT-Large)GroupNormOp- Group normalization for modern CNNs (PyTorch compatible)InstanceNormOp- Instance normalization for style transfer and GANs (PyTorch compatible)RMSNormOp- Root Mean Square normalization for LLMs (LLaMA, Gemma)
Layout
PermuteOp- Axis reordering (e.g., HWC to CHW)ToTensorOp- HWC uint8 to CHW float32 with optional scalingToImageOp- CHW float32 to HWC uint8
Data Augmentation
RandomCropOp- Random cropping with deterministic seed supportGaussianBlurOp- Gaussian blur using separable convolutionRandomHorizontalFlipOp/RandomVerticalFlipOp- Probabilistic flip augmentationHorizontalFlipOp/VerticalFlipOp- Deterministic flip operationsRandomErasingOp- Random erasing (cutout) augmentationColorJitterOp- Random brightness, contrast, saturation, and hue jitterAdjustBrightnessOp/AdjustContrastOp/AdjustSaturationOp/AdjustHueOp- Individual color adjustments
Color Space Conversion
RgbToGrayscaleOp- RGB to grayscale using ITU-R BT.601 coefficients (torchvisionGrayscale,tf.image.rgb_to_grayscale)RgbToHsvOp- RGB to HSV color space conversion (tf.image.rgb_to_hsv)HsvToRgbOp- HSV to RGB color space conversion (tf.image.hsv_to_rgb)
Fused Operations
ResizeNormalizeFusedOp- Combines resize + normalize in single pass (eliminates intermediate tensor)
Activation Functions
ReLUOp- Rectified Linear Unit (SIMD accelerated)LeakyReLUOp- Leaky ReLU with configurable slope (SIMD accelerated)GELUOp- Gaussian Error Linear Unit (Transformers: BERT, GPT, ViT)SiLUOp/SwishOp- Sigmoid Linear Unit (EfficientNet, YOLOv5)HardsigmoidOp- Hardware-efficient sigmoid (MobileNetV3)HardswishOp- Hardware-efficient swish (MobileNetV3)MishOp- Self-regularizing activation (YOLOv4+)ELUOp- Exponential Linear UnitSELUOp- Scaled Exponential Linear UnitGLUOp- Gated Linear UnitSigmoidOp- Sigmoid activationTanhOp- Hyperbolic tangent activationSoftmaxOp- Softmax along specified axis
Math Operations
AbsOp- Absolute value (SIMD accelerated)NegOp- Negation (SIMD accelerated)SqrtOp- Square root (SIMD accelerated)ExpOp- Exponential (e^x)LogOp- Natural logarithmPowOp- Power operationFloorOp/CeilOp/RoundOp- Element-wise rounding operationsSinOp/CosOp/TanOp- Trigonometric functionsAsinOp/AcosOp/AtanOp/Atan2Op- Inverse trigonometric functions
Arithmetic Operations
AddOp/SubOp- Element-wise addition/subtraction (SIMD accelerated)MulOp/DivOp- Element-wise multiplication/division (SIMD accelerated)
Normalization (continued)
LpNormalizeOp- Lp normalization (L1, L2, Linf) along a dimension
Tensor Manipulation
tensorWhere()/WhereOp- Element-wise conditional selectionMaskedFillOp- Fill tensor positions where mask is trueGatherOp- Gather elements along a dimension by indexTopKOp- Select k largest/smallest values and indices along an axis (quickselect)ArgMaxOp/ArgMinOp- Index of max/min value along an axis (ONNX ArgMax/ArgMin opset 13)TileOp- Tile/repeat tensor contentsRepeatOp- Repeat tensor (PyTorch.repeat()semantics)RollOp- Circular shift along dimensionsPositionalEncodingOp- Transformer positional encoding
Utility
concat()- Concatenates tensors along specified axisstack()- Stacks tensors along a new dimensionsplit()/chunk()- Split tensor into parts along a dimension
Shape
UnsqueezeOp- Add dimensionSqueezeOp- Remove size-1 dimensionsReshapeOp- Reshape tensor (supports -1 for inference)FlattenOp- Flatten dimensions
Type
TypeCastOp- Convert between data types
Core Classes
TensorBuffer
Tensor with shape and stride metadata over physical storage.
// Create tensors
final zeros = TensorBuffer.zeros([3, 224, 224]);
final ones = TensorBuffer.ones([3, 224, 224], dtype: DType.float32);
final fromData = TensorBuffer.fromFloat32List(data, [3, 224, 224]);
// Access elements
final value = tensor[[0, 100, 100]];
// Zero-copy operations
final transposed = tensor.transpose([2, 0, 1]); // Changes strides only
final squeezed = tensor.squeeze();
// Copy operations
final contiguous = tensor.contiguous(); // Force contiguous memory
final cloned = tensor.clone();
DType
ONNX-compatible data types with onnxId for runtime integration.
DType.float32 // ONNX ID: 1
DType.int64 // ONNX ID: 7
DType.uint8 // ONNX ID: 2
BufferPool
Memory pooling for buffer reuse, reducing GC pressure in hot paths.
final pool = BufferPool.instance;
// Acquire buffer (reuses from pool if available)
final buffer = pool.acquireFloat32(1000);
// ... use buffer ...
// Release back to pool for reuse
pool.release(buffer);
// Monitor pool usage
print('Pooled: ${pool.pooledCount} buffers, ${pool.pooledBytes} bytes');
Zero-Copy View Operations
TensorBuffer extension methods for zero-copy tensor manipulation:
// Slice along first dimension (batch slicing)
final batch = tensor.sliceFirst(2, 5); // Views elements 2..4
// Split tensor into views
final items = tensor.unbind(0); // List of views along dim 0
// Select single index (reduces rank)
final first = tensor.select(0, 0); // First item, shape reduced
// Narrow dimension
final narrowed = tensor.narrow(0, 1, 3); // 3 elements starting at 1
// Format conversion without copying
final nhwc = nchwTensor.toChannelsLast(); // NCHW -> NHWC view
final nchw = nhwcTensor.toChannelsFirst(); // NHWC -> NCHW view
// Flatten to 1D view
final flat = tensor.flatten();
In-Place Operations
Many operations support in-place modification to avoid allocation overhead:
// In-place operations (modify tensor directly)
ReLUOp().applyInPlace(tensor);
NormalizeOp.imagenet().applyInPlace(tensor);
ClipOp(min: 0, max: 1).applyInPlace(tensor);
BatchNormOp(...).applyInPlace(tensor);
// Query operation capabilities
final op = ReLUOp();
print(op.capabilities.supportsInPlace); // true
print(op.capabilities.requiresContiguous); // true
print(op.capabilities.preservesShape); // true
Operations supporting in-place: ReLUOp, LeakyReLUOp, SigmoidOp, TanhOp, AbsOp, NegOp, SqrtOp, ExpOp, LogOp, PowOp, AddOp, SubOp, MulOp, DivOp, ClipOp, NormalizeOp, ScaleOp, BatchNormOp, LayerNormOp, GroupNormOp, InstanceNormOp, RMSNormOp, SELUOp, LpNormalizeOp, MaskedFillOp, RandomErasingOp.
Memory Formats
| Format | Layout | Strides (for 1,3,224,224) |
|---|---|---|
contiguous |
NCHW | 150528, 50176, 224, 1 |
channelsLast |
NHWC | 150528, 1, 672, 3 |
PyTorch Compatibility
Compatibility is checked against pinned CPU PyTorch 2.10.0 / torchvision 0.25.0 goldens. Floating-point results use documented absolute/relative tolerances, not bitwise equality. See fixture provenance and reproduction. The table maps APIs to their reference operations; it does not imply that every PyTorch dtype, input rank or option is supported.
Migration to 1.0.0
NormalizeOp and ResizeNormalizeFusedOp copy and freeze their mean/std lists.
Recreate the operation to change statistics. Gaussian blur, random erasing and
color adjustments now reject invalid non-finite parameters; shape inference
performs the same rank/channel checks as execution. PadMode.reflect keeps
edge-inclusive symmetric boundaries, including repeated reflection for large
padding. Color and random augmentation contracts that intentionally differ
from torchvision are listed in COMPATIBILITY.md.
Low-level mutation dispatch rejects non-contiguous destinations; copy to contiguous storage first. SIMD binary kernels validate equal lengths in release builds. BufferPool release transfers ownership: stop using the buffer and its aliases until it is acquired again.
LayoutConvertOp.toNhwc() explicitly expects NCHW input; toNchw() expects
NHWC input. Both always permute logical axes, independently of physical
memoryFormat. Previously toNchw could silently skip conversion. Physical
channels-last storage retains logical CHW/NCHW shape; it does not label a tensor
as logically HWC/NHWC. Use PermuteOp or these directional helpers for axis changes.
Tensor shapes and strides are now immutable copies. Constructors reject views
outside storage, negative strides and inconsistent stride counts. Zero strides
remain valid for read-only broadcast views. Rank-zero and empty tensors remain
unsupported: squeezing a single element now retains [1], so clone and reshape
continue to work. Squeeze/unsqueeze accept negative axes and reject invalid axes.
eye, linspace and arange now allocate the requested dtype. Sequence
factories use double arithmetic and truncate integer outputs toward zero;
their parameters must be finite and produce a nonempty sequence. TypeCastOp
retains its legacy half-away rounding/clamping rules, but integer-to-integer
casts no longer lose precision by converting through double.
Axis reductions no longer silently cast every output to float32: float32/64
remain their input dtype, integer sum promotes to int64, and min/max retain the
input dtype. Integer axis mean is rejected; cast to float first. Scalar-valued
sum/mean/min/max still explicitly return double, and tensor reductions with
no remaining dimensions return shape [1]. Empty multi-axis lists remain an
identity operation. Argmin/argmax compare integers exactly and return the first
NaN index when present. Top-k treats NaN as largest; tied indices are unordered.
Gather requires integer indices (including int32, a package extension) and
validates non-gather dimensions. Tile/repeat require one positive count per axis.
random and randn support float32/float64 and reject integer dtypes. Seeds
are reproducible within this package, not equivalent to PyTorch seeds. Correcting
the uniform endpoint and Box-Muller math changes seeded outputs from 0.9.0.
LpNormalizeOp now divides by max(norm, eps) and accepts positive p (including
infinity) with finite positive eps. Other normalization eps values must also
be finite and positive. int64/uint64 clone and contiguous copies preserve exact
integers; the explicitly double-valued indexing API still returns double.
- Presets accept RGB HWC/NHWC
uint8pixels orfloat32/float64values already in0,1. Outputs are float32. Grayscale and RGBA require explicit RGB conversion. - Presets convert to CHW/NCHW before resizing, preserve an existing batch, and
antialias bilinear/bicubic resizing.
tflite()returns NHWC;custom(toChw: false)returns channel-last output after normalization. - Low-level
ResizeOpstill defaults toantialias: false. Its default nearest mode now follows PyTorchnearest(asymmetric coordinates); an explicitcoordinateModepreserves access to ONNX-style alternatives. Antialias accepts float32/64 with half-pixel or align-corners coordinates. Integer antialias inputs must first be converted to float. - Bicubic uses PyTorch's coefficient -0.75 without antialias and -0.5 with antialias. Area uses adaptive-average bins. Shortest-edge sizes truncate; center crops use round-to-even and zero-pad oversized requests.
- These numerical corrections can change model inputs compared with 0.9.0. Presets are explicit tensor recipes. They do not guarantee parity with every pretrained weight configuration or Pillow-based processor; object detection uses direct resizing, without letterboxing.
Reference operations
| Operation | PyTorch Equivalent |
|---|---|
TensorBuffer.zeros() |
torch.zeros() |
TensorBuffer.ones() |
torch.ones() |
tensor.transpose() |
tensor.permute() |
tensor.reshape() |
tensor.reshape() |
tensor.squeeze() |
tensor.squeeze() |
tensor.unsqueeze() |
tensor.unsqueeze() |
tensor.sum() / sumAxis() |
tensor.sum() |
tensor.sumAxes([...]) |
tensor.sum(dim=[...]) |
tensor.mean() / meanAxis() |
tensor.mean() |
tensor.meanAxes([...]) |
tensor.mean(dim=[...]) |
tensor.min() / max() |
tensor.min() / max() |
tensor.minAxes([...]) |
tensor.amin(dim=[...]) |
tensor.maxAxes([...]) |
tensor.amax(dim=[...]) |
NormalizeOp.imagenet() |
transforms.Normalize(mean, std) |
ResizeOp(mode: bilinear) |
F.interpolate(mode='bilinear') |
ResizeOp(mode: area) |
F.interpolate(mode='area') |
ResizeOp(mode: lanczos) |
Lanczos3 interpolation |
ResizeOp(coordinateMode: halfPixel) |
ONNX Resize half_pixel |
ResizeOp(coordinateMode: asymmetric) |
ONNX Resize asymmetric (TF default) |
ResizeOp(coordinateMode: pytorchHalfPixel) |
ONNX Resize pytorch_half_pixel |
ToTensorOp() |
transforms.ToTensor() |
ClipOp(min, max) |
torch.clamp(min, max) |
PadOp(mode: reflect) |
F.pad(mode='reflect') |
SliceOp([(start, end, step)]) |
tensor[start:end:step] |
concat(tensors, axis) |
torch.cat(tensors, dim) |
stack(tensors, dim) |
torch.stack(tensors, dim) |
RandomCropOp |
transforms.RandomCrop() |
GaussianBlurOp |
transforms.GaussianBlur() |
AddOp / SubOp |
torch.add() / torch.sub() |
MulOp / DivOp |
torch.mul() / torch.div() |
PowOp |
torch.pow() |
AbsOp / NegOp |
torch.abs() / torch.neg() |
SqrtOp / ExpOp / LogOp |
torch.sqrt() / exp() / log() |
FloorOp / CeilOp / RoundOp |
torch.floor() / ceil() / round() |
SinOp / CosOp / TanOp |
torch.sin() / cos() / tan() |
AsinOp / AcosOp / AtanOp |
torch.asin() / acos() / atan() |
Atan2Op |
torch.atan2() |
ReLUOp / LeakyReLUOp |
F.relu() / F.leaky_relu() |
GELUOp |
F.gelu() |
SiLUOp / SwishOp |
F.silu() |
HardsigmoidOp |
F.hardsigmoid() |
HardswishOp |
F.hardswish() |
MishOp |
F.mish() |
ELUOp |
F.elu() |
SigmoidOp / TanhOp |
torch.sigmoid() / torch.tanh() |
SoftmaxOp |
F.softmax() |
SELUOp |
F.selu() |
GLUOp |
F.glu() |
BatchNormOp |
torch.nn.BatchNorm2d (inference) |
LayerNormOp |
torch.nn.LayerNorm |
GroupNormOp |
torch.nn.GroupNorm |
InstanceNormOp |
torch.nn.InstanceNorm2d |
RMSNormOp |
torch.nn.RMSNorm (PyTorch 2.4+) |
TensorBuffer.full() |
torch.full() |
TensorBuffer.random() |
torch.rand() |
TensorBuffer.randn() |
torch.randn() |
TensorBuffer.eye() |
torch.eye() |
TensorBuffer.linspace() |
torch.linspace() |
TensorBuffer.arange() |
torch.arange() |
tensor.select(dim, index) |
tensor.select(dim, index) |
tensor.narrow(dim, start, len) |
tensor.narrow(dim, start, len) |
tensor.unbind(dim) |
tensor.unbind(dim) |
tensor.flatten() |
tensor.flatten() |
LpNormalizeOp |
F.normalize() |
tensorWhere() / WhereOp |
torch.where() |
MaskedFillOp |
Tensor.masked_fill_() |
GatherOp |
torch.gather() |
TopKOp / tensor.topk() |
torch.topk() |
tensor.argmax() / tensor.argmin() |
tensor.argmax() / tensor.argmin() |
ArgMaxOp / ArgMinOp |
torch.argmax() / torch.argmin() |
split() / chunk() |
torch.split() / torch.chunk() |
TileOp |
Tensor.repeat() / ONNX Tile |
RepeatOp |
Tensor.repeat() |
RollOp |
torch.roll() |
RandomHorizontalFlipOp |
transforms.RandomHorizontalFlip() |
RandomVerticalFlipOp |
transforms.RandomVerticalFlip() |
RandomErasingOp |
transforms.RandomErasing() |
ColorJitterOp |
transforms.ColorJitter() |
PositionalEncodingOp |
Transformer positional encoding |
RgbToGrayscaleOp |
torchvision.transforms.Grayscale() / tf.image.rgb_to_grayscale |
RgbToHsvOp |
tf.image.rgb_to_hsv |
HsvToRgbOp |
tf.image.hsv_to_rgb |
ResizeNormalizeFusedOp |
F.interpolate() + transforms.Normalize() (fused) |
Performance Benchmarks
Run benchmarks with dart run benchmark/run_all.dart.
SIMD Acceleration
Operations with Float32x4/Float64x2 SIMD vectorization:
| Operation | SIMD Throughput | Speedup |
|---|---|---|
ClipOp |
~6.2 GE/s (Float32) | ~4x |
AbsOp |
~6.2 GE/s (Float32) | ~4x |
SqrtOp |
~6.2 GE/s (Float32) | ~4x |
NormalizeOp |
~6.2 GE/s (Float32) | ~4x |
ReLUOp / LeakyReLUOp |
~6.2 GE/s (Float32) | ~4x |
ScaleOp |
~6.2 GE/s (Float32) | ~4x |
AddOp / SubOp / MulOp / DivOp |
~6.2 GE/s (Float32) | ~4x |
GE/s = Giga Elements per second. Float64 SIMD achieves ~53% of Float32 performance due to Float64x2 vs Float32x4.
Operation Complexity
| Operation | Time Complexity | Space Complexity |
|---|---|---|
ResizeOp (bilinear) |
O(C × H × W) | O(C × H × W) |
ResizeOp (bicubic) |
O(C × H × W × 16) | O(C × H × W) |
ResizeOp (lanczos) |
O(C × H × W × 36) | O(C × H × W) |
NormalizeOp |
O(n) | O(n) or O(1) in-place |
BatchNormOp |
O(n) | O(n) or O(1) in-place |
LayerNormOp |
O(n) | O(n) or O(1) in-place |
GaussianBlurOp |
O(C × H × W × k) | O(C × H × W) |
ResizeNormalizeFusedOp |
O(C × H × W) | O(C × H × W) |
Zero-Copy Operations (O(1))
| Operation | Time | Ops/sec |
|---|---|---|
transpose() |
~1µs | 700K+ |
reshape() |
~1µs | 1.6M+ |
squeeze() |
<1µs | 3.2M+ |
unsqueeze() |
~1µs | 780K+ |
Pipeline Performance
| Pipeline | Input Shape | Time |
|---|---|---|
| Simple (Normalize + Unsqueeze) | 3, 224, 224 |
~3.4ms |
| ImageNet Classification | 3, 224, 224 |
~3.0ms |
| Object Detection | 3, 640, 640 |
~25ms |
Sync vs Async
| Execution | 224x224 | 640x640 |
|---|---|---|
run() (sync) |
~3.5ms | ~29ms |
runAsync() (isolate) |
~11ms | ~93ms |
| Isolate overhead | ~7ms | ~64ms |
Note: Use
runAsync()for large tensors or when UI responsiveness is critical.
CI
This project uses GitHub Actions for continuous integration:
| Job | Description | Dart SDK |
|---|---|---|
| Static Analysis | dart analyze --fatal-infos |
stable |
| Format Check | dart format --set-exit-if-changed |
stable |
| Tests | dart test |
3.9.0, stable |
Requirements
- Dart SDK ^3.9.0
License
MIT
Libraries
- dart_tensor_preprocessing
- A high-performance tensor preprocessing library for Flutter/Dart.