Vision

Toro.Vision provides composable image transforms, image I/O (backed by TorchVision), and bitmap-level preprocessing via SkiaSharp. Open the namespace to get started:

open Toro
open Toro.Vision

Image I/O

The Image module provides image loading and saving via TorchVision, plus SKBitmap-Tensor conversion via SkiaSharp.

Loading

// Load from file as [3, H, W] float32 in [0, 1]:
let img = Image.load "photo.jpg" Cpu

// Load from stream (uses SkiaSharp internally):
use stream = File.OpenRead("photo.png")
let img = Image.loadStream stream Cpu

Saving

// Save a [3, H, W] tensor as JPEG:
Image.save tensor "output.jpg" Jpeg 0

// Save as PNG:
Image.save tensor "output.png" Png 0

The quality parameter is reserved for future use.

Saving a Grid

Save a batch of images as a grid (wraps torchvision.utils.save_image):

// Save [N, C, H, W] batch as a grid with 8 images per row:
Image.saveGrid batch "grid.png" Png 0 8

Bitmap-Tensor Conversion

Convert between SkiaSharp's SKBitmap and Toro Tensor directly:

open SkiaSharp

// SKBitmap → Tensor [3, H, W] float32 [0, 1]:
let bitmap = SKBitmap.Decode("photo.jpg")
let tensor = Image.toTensor bitmap Cpu

// Tensor → SKBitmap:
let bmp = Image.fromTensor tensor

SkiaTransform (Bitmap-Level Pipeline)

The SkiaTransform module provides spatial transforms operating directly on SKBitmap. Use this for CPU-side preprocessing before tensor conversion — SkiaSharp's rasterizer handles resize and crop without allocating GPU tensors.

open SkiaSharp

let bitmap = SKBitmap.Decode("photo.jpg")
let resized = SkiaTransform.resize 256 256 bitmap
let cropped = SkiaTransform.centerCrop 224 224 resized
let flipped = SkiaTransform.flipH cropped
let tensor = Image.toTensor flipped Cpu

Functions include resize, centerCrop, randomCrop, flipH, flipV, and pipeline. See the SkiaTransform API reference for signatures.

Pipeline

Chain bitmap transforms and convert to tensor in one call. Intermediate bitmaps are disposed automatically:

let tensor =
    SkiaTransform.pipeline
        [ SkiaTransform.resize 256 256; SkiaTransform.flipH ]
        bitmap
        Cpu

When to Use SkiaTransform vs. ITransform

SkiaTransform operates on SKBitmap (CPU pixels) and is best for heavy spatial transforms before tensorization. ITransform operates on Tensor (CPU or GPU) and is better for augmentations in the training loop. Both approaches can be combined: apply bitmap transforms first, convert to tensor, then apply tensor transforms.

ITransform (Tensor Transforms)

All tensor transforms implement ITransform and expose a direct apply member:

type ITransform =
    abstract apply: Tensor -> Tensor

Each transform record has a public member apply — you can call it directly without casting:

let norm = Normalize.imageNet
let result = norm.apply tensor   // no cast needed

Input tensors are expected in [C, H, W] or [B, C, H, W] format with float32 values.

Available Transforms

Normalize

Channel-wise normalization: (xμ)/σ(x - \mu) / \sigma.

let norm = Normalize.imageNet
let out = norm.apply img

Resize

Resize spatial dimensions using bilinear interpolation:

let resize = Resize.create 224 224
let out = resize.apply img

RandomHorizontalFlip

Flip the image horizontally with a given probability:

let flip = RandomHorizontalFlip.create 0.5
let flip = RandomHorizontalFlip.defaultFlip

RandomVerticalFlip

Flip the image vertically with a given probability:

let flip = RandomVerticalFlip.create 0.5
let flip = RandomVerticalFlip.defaultFlip

RandomCrop

Randomly crop the image to the specified size:

let crop = RandomCrop.create 224 224

Throws if the input is smaller than the crop size.

CenterCrop

Deterministically crop the center region of the image:

let crop = CenterCrop.create 224 224

Throws if the input is smaller than the crop size.

ToGrayscale

Convert an RGB image to grayscale using ITU-R BT.601 luminance weights (0.2989R+0.587G+0.114B0.2989 R + 0.587 G + 0.114 B). Output can be single-channel or three identical channels:

let gray = ToGrayscale.single
let gray = ToGrayscale.triple

Input must have exactly 3 channels.

ConvertImageDType

Convert image tensor dtype with automatic value scaling between integer [0, 255] and float [0.0, 1.0] ranges:

let toFloat = ConvertImageDType.create F32
let toInt = ConvertImageDType.create U8

Compose

Chain multiple transforms into a single pipeline with Compose.apply. Items in the list must be typed as ITransform:

let img = Image.load "photo.jpg" Cpu

let transforms: ITransform list = [
    Resize.create 224 224
    RandomHorizontalFlip.defaultFlip
    Normalize.imageNet
]

let processed = Compose.apply transforms img

Training vs. Inference Pipeline

let trainTransforms: ITransform list = [
    Resize.create 256 256
    RandomCrop.create 224 224
    RandomHorizontalFlip.defaultFlip
    RandomVerticalFlip.defaultFlip
    Normalize.imageNet
]

let evalTransforms: ITransform list = [
    Resize.create 256 256
    CenterCrop.create 224 224
    Normalize.imageNet
]

Full Example: Load, Transform, Classify

open Toro
open Toro.Vision
open SkiaSharp

// Bitmap preprocessing (CPU, no tensor allocation):
let bitmap = SKBitmap.Decode("cat.jpg")
let resized = SkiaTransform.resize 256 256 bitmap

// Convert to tensor and apply tensor transforms:
let tensor = Image.toTensor resized Cpu
let cropped = (CenterCrop.create 224 224).apply tensor
let normalized = Normalize.imageNet.apply cropped

// Feed to model...