Rasterization Guide
This guide covers spatial-to-sequence rasterization in medlatents, enabling autoregressive modeling of 2D/3D spatial data like medical images and volumes.
Overview
Autoregressive models process sequences, but medical data is spatial (2D/3D). Rasterization converts spatial data to sequences while preserving locality:
flowchart LR
subgraph Spatial["Spatial Data"]
I[2D Image / 3D Volume]
end
subgraph Rasterize["Rasterization"]
I --> R[Space-Filling Curve]
end
subgraph Sequence["Sequence"]
R --> S[1D Token Sequence]
end
subgraph Generate["Generation"]
S --> AR[Autoregressive Model]
AR --> S2[Generated Sequence]
end
subgraph Unraster["Unrasterization"]
S2 --> I2[Reconstructed Spatial]
end
Why Locality Matters
Adjacent tokens should represent nearby spatial locations:
Better context: Autoregressive models use previous tokens as context
Faster learning: Local patterns are easier to learn
Inpainting: Masked regions should have nearby context
Available Methods
RasterScan
Simple row-by-row (or slice-by-slice) scanning.
from medlatents.rasterization import RasterScan
# For 2D images
rasterizer = RasterScan(
height=64,
width=64,
order='row', # Options: row, column
)
# For 3D volumes
rasterizer = RasterScan(
depth=32,
height=64,
width=64,
order='slice', # Options: slice, row, column
)
# Convert image to sequence
image = torch.randn(1, 64, 64) # [C, H, W]
sequence = rasterizer.rasterize(image) # [1, H*W]
# Convert back
reconstructed = rasterizer.unrasterize(sequence) # [C, H, W]
Properties:
[+] Simple and fast
[+] No computation overhead
[-] Poor locality at row/column boundaries
SCurve (Serpentine)
Boustrophedon pattern - alternating left-right, right-left rows.
from medlatents.rasterization import SCurve
rasterizer = SCurve(
height=64,
width=64,
)
sequence = rasterizer.rasterize(image)
Properties:
[+] Simple
[+] Better than raster at row boundaries
[-] Still has locality issues at row ends
HilbertCurve
Space-filling curve with optimal locality preservation.
from medlatents.rasterization import HilbertCurve
# Dimensions should be powers of 2
rasterizer = HilbertCurve(
height=64, # Must be power of 2
width=64, # Must be power of 2
)
sequence = rasterizer.rasterize(image)
# 3D Hilbert curve
rasterizer_3d = HilbertCurve(
depth=32,
height=64,
width=64,
)
Properties:
[+] Best locality (adjacent sequence = adjacent spatial)
[+] Self-similar structure
[!] Requires power-of-2 dimensions
[!] More computation to generate curve
ZOrderCurve (Morton)
Z-order (Morton) curve using bit interleaving.
from medlatents.rasterization import ZOrderCurve, rasterize_2d, unrasterize_2d
rasterizer = ZOrderCurve(
height=64,
width=64,
)
sequence = rasterizer.rasterize(image)
# Quick functions for common cases
sequence = rasterize_2d(image, method='zorder')
image = unrasterize_2d(sequence, height=64, width=64, method='zorder')
Properties:
[+] Good locality (better than raster/S-curve)
[+] Fast (bit operations)
[+] Works with non-power-of-2 (with padding)
[!] Slightly worse locality than Hilbert
Comparison
Visual Comparison
Raster Scan: S-Curve: Hilbert: Z-Order:
→ → → → ↓ → → → → ↓ ┌─┐ ┌─┐ ↘ ↓ ↘ ↓
→ → → → ↓ ← ← ← ← ↓ │ └─┘ │ → ↘ → ↘
→ → → → ↓ → → → → ↓ └─┐ ┌─┘ ↘ ↓ ↘ ↓
→ → → → ↓ ← ← ← ← ↓ └─┘ → ↘ → ↘
Locality Metrics
Method |
Avg Distance |
Max Distance |
Computation |
|---|---|---|---|
Raster |
O(W) |
O(W) |
O(1) |
S-Curve |
O(1) / O(W) |
O(W) |
O(1) |
Hilbert |
O(1) |
O(√N) |
O(N) |
Z-Order |
O(1) / O(log N) |
O(√N) |
O(N) |
Practical Usage
With Autoregressive Models
from medlatents.rasterization import HilbertCurve
from medlatents.autoregressive import AutoregressiveTransformer
# Setup
rasterizer = HilbertCurve(height=64, width=64)
model = AutoregressiveTransformer(
vocab_size=8192,
max_seq_len=64 * 64, # H * W
)
# Training
for batch in dataloader:
tokens = batch['tokens'] # [B, C, H, W] image tokens
# Rasterize each channel
sequences = []
for c in range(tokens.shape[1]):
seq = rasterizer.rasterize(tokens[:, c]) # [B, H*W]
sequences.append(seq)
sequence = torch.cat(sequences, dim=1) # [B, C*H*W]
# Train autoregressive model
loss = model.compute_loss(sequence)
# Generation
generated_seq = model.generate(max_length=64 * 64)
generated_image = rasterizer.unrasterize(generated_seq)
With Tokenizers
from medlatents.data import VQTokenizer
from medlatents.rasterization import ZOrderCurve
# Tokenize image to discrete codes
tokenizer = VQTokenizer.from_pretrained('path/to/vq')
tokens = tokenizer.encode(image) # [1, H//p, W//p] where p is patch size
# Rasterize
rasterizer = ZOrderCurve(
height=tokens.shape[1],
width=tokens.shape[2],
)
sequence = rasterizer.rasterize(tokens)
3D Medical Volumes
from medlatents.rasterization import HilbertCurve
# 3D MRI volume
rasterizer = HilbertCurve(
depth=128, # Slices
height=256, # Rows
width=256, # Columns
)
volume = torch.randn(1, 128, 256, 256) # [C, D, H, W]
sequence = rasterizer.rasterize(volume) # [1, D*H*W]
# Very long sequences! Consider:
# 1. Downsampling first
# 2. Using sliding windows
# 3. Hierarchical generation
Batch Processing
# All methods support batched operations
batch = torch.randn(32, 1, 64, 64) # [B, C, H, W]
rasterizer = HilbertCurve(64, 64)
# Rasterize batch
sequences = rasterizer.rasterize(batch) # [B, 1, H*W]
# Unrasterize batch
images = rasterizer.unrasterize(sequences) # [B, 1, H, W]
Choosing a Method
Decision Guide
Is dimension power of 2?
├── Yes
│ └── Need best locality?
│ ├── Yes → HilbertCurve
│ └── No → ZOrderCurve (faster)
└── No
└── Can pad to power of 2?
├── Yes → Hilbert/ZOrder with padding
└── No → SCurve (if 2D) or RasterScan
Recommendations by Use Case
Use Case |
Recommended |
Reason |
|---|---|---|
Medical images (2D) |
HilbertCurve |
Best locality for pathology |
Medical volumes (3D) |
HilbertCurve |
Preserves 3D structure |
Fast prototyping |
RasterScan |
Simple, no overhead |
Non-power-of-2 |
ZOrderCurve |
Handles any size with padding |
Sliding windows |
RasterScan |
Predictable boundaries |
Handling Non-Power-of-2 Dimensions
Padding Strategy
import math
def next_power_of_2(n):
return 2 ** math.ceil(math.log2(n))
# Original size
H, W = 100, 150
# Pad to power of 2
H_padded = next_power_of_2(H) # 128
W_padded = next_power_of_2(W) # 256
# Pad image
image_padded = F.pad(image, (0, W_padded - W, 0, H_padded - H))
# Rasterize
rasterizer = HilbertCurve(H_padded, W_padded)
sequence = rasterizer.rasterize(image_padded)
# After generation, crop back
generated = rasterizer.unrasterize(generated_seq)
generated = generated[:, :, :H, :W]
Tile-Based Approach
For very large images, process in tiles:
def process_tiles(image, tile_size=64):
"""Process image in tiles with overlap."""
B, C, H, W = image.shape
rasterizer = HilbertCurve(tile_size, tile_size)
tiles = []
for i in range(0, H, tile_size):
for j in range(0, W, tile_size):
tile = image[:, :, i:i+tile_size, j:j+tile_size]
tile_seq = rasterizer.rasterize(tile)
tiles.append(tile_seq)
return torch.cat(tiles, dim=1)
Performance Tips
Caching Indices
For repeated use, cache the rasterization indices:
rasterizer = HilbertCurve(64, 64)
# Pre-compute indices (done automatically on first call)
_ = rasterizer.get_indices()
# Now rasterization is just indexing (fast)
for batch in dataloader:
sequence = rasterizer.rasterize(batch['image'])
GPU Acceleration
All operations are GPU-compatible:
rasterizer = HilbertCurve(64, 64)
image = image.cuda()
sequence = rasterizer.rasterize(image) # Stays on GPU
Compiled Operations
# Compile for speed (PyTorch 2.0+)
rasterize_fn = torch.compile(rasterizer.rasterize)
sequence = rasterize_fn(image)
API Reference
See also
Rasterization API - Full API documentation
Autoregressive Models - Autoregressive models
MaskGIT - MaskGIT (alternative to AR)