Vision & Multimodal AI
The vision space, laid out end to end across seven subdomains — from pixel arithmetic through recognition, generation, models that reason across modalities, and output that has to drive something interactive at frame rate. Each subdomain runs from foundations to advanced, so you can enter wherever you already are.
Click a subdomain to open it, then any concept inside. PyTorch code for everything is collected at the end. Tiers are cumulative: advanced material generally assumes the foundations above it.
1 · Image Processing 2 · Computer Vision 3 · Diffusion Models 4 · Vision-Language Models 5 · Multimodal Language Modelling 6 · Generative Vision — Autoencoders & GANs 7 · Animation, Audio & Real-Time 3D Code snippets
1 · Image Processing
Libraries & packagesopencv-python (cv2) numpy scikit-image Pillow scipy.ndimage PyWavelets
Operating on images as signals rather than as scenes. Everything downstream inherits these representations, and a surprising share of "model" problems turn out to be preprocessing problems.
Foundations
Digital image representation
An image is a grid of pixels, each holding one value per channel at some bit depth — typically 8-bit RGB, giving 256 levels per channel. Height × width × channels is the tensor every later stage consumes.
Bit depth matters more than people expect: 8-bit is fine for display but clips highlights and quantises gradients, which is why medical and scientific imaging works in 12- or 16-bit.
Uses: numpy · cv2.imread · PIL.Image.open
Colour spaces
RGB is how displays emit light, not how anything perceives it. HSV separates hue from saturation and brightness, making colour thresholding tractable; LAB is designed so that equal numeric distances correspond to roughly equal perceptual differences.
Choosing the right space often converts a hard problem into a trivial one — segmenting a coloured object is painful in RGB and nearly free in HSV, because illumination changes move all three RGB channels at once.
Uses: cv2.cvtColor · skimage.color
Histograms & equalization
A histogram counts pixel intensities. Its shape tells you immediately whether an image is under- or over-exposed, low contrast, or clipped.
Equalization redistributes intensities to spread the histogram, and CLAHE does it in local tiles so one bright region does not wash out the rest. It is a standard preprocessing step wherever illumination varies across a dataset.
Uses: cv2.calcHist · cv2.equalizeHist · cv2.createCLAHE
Point operations
Operations applied to each pixel independently of its neighbours — brightness offsets, contrast scaling, gamma correction, inversion, and lookup-table mappings.
Gamma is the one that matters most and is most often ignored: pixel values are usually stored non-linearly, so averaging or blending raw values is arithmetically wrong unless you linearise first.
Uses: numpy · cv2.LUT · cv2.convertScaleAbs
Convolution & kernels
Convolution slides a small matrix over the image, replacing each pixel with a weighted sum of its neighbourhood. The kernel's values determine the operation — blur, sharpen, or edge response.
This is the single most important operation in the field. The only difference between classical image processing and a CNN is whether the kernel weights are designed or learned.
Uses: cv2.filter2D · scipy.ndimage.convolve
Smoothing & noise reduction
Gaussian blur averages with distance-weighted neighbours; median filtering replaces each pixel with the neighbourhood median and is far better against salt-and-pepper noise; bilateral filtering weights by both spatial and intensity distance.
Bilateral is the useful one when you want denoising without destroying edges — it refuses to average across a large intensity jump, which is exactly what Gaussian blur does wrong.
Uses: cv2.GaussianBlur · cv2.medianBlur · cv2.bilateralFilter
Edge detection
Edges are intensity discontinuities, found by approximating spatial derivatives. Sobel and Prewitt give gradient magnitude and direction; Laplacian responds to second derivatives.
Canny remains the standard because it is a pipeline rather than a filter: smooth, compute gradients, suppress non-maxima to thin edges, then hysteresis-threshold so weak edges survive only when connected to strong ones.
Uses: cv2.Canny · cv2.Sobel · cv2.Laplacian
Core
Thresholding & binarisation
Reducing an image to foreground and background. A global threshold works only under even illumination; Otsu's method picks it automatically by maximising between-class variance.
Adaptive thresholding computes a local threshold per region and is what makes OCR work on photographed documents, where one corner is always darker than the other.
Uses: cv2.threshold · cv2.adaptiveThreshold · skimage.filters.threshold_otsu
Morphological operations
Shape operations on binary images using a structuring element. Erosion shrinks foreground, dilation grows it; opening (erode then dilate) removes small specks, and closing (dilate then erode) fills small holes.
These are the cheap, reliable cleanup tools after thresholding or segmentation — removing isolated noise pixels and closing gaps in contours that would otherwise break connected-component analysis.
Uses: cv2.erode · cv2.dilate · cv2.morphologyEx
Image pyramids & multi-scale
A pyramid is the same image at successively lower resolutions. Gaussian pyramids downsample after blurring; Laplacian pyramids store the difference between levels, capturing detail per scale.
Scale is the recurring problem in vision — an object's size in pixels is arbitrary — and pyramids are the classical answer, echoed later by feature pyramid networks in detection.
Uses: cv2.pyrDown · cv2.pyrUp · skimage.transform.pyramid_gaussian
Frequency domain & Fourier
The Fourier transform re-expresses an image as a sum of spatial frequencies. Low frequencies carry broad structure, high frequencies carry edges and texture.
It makes some operations trivial — periodic noise becomes an isolated spike you can remove, and convolution becomes multiplication — and it explains aliasing, which is why downsampling without blurring first produces moiré artefacts.
Uses: numpy.fft · cv2.dft · scipy.fft
Geometric transforms & interpolation
Translation, rotation, scaling, affine, and perspective (homography) transforms map pixels to new positions. Because the mapping lands between pixels, the value must be interpolated — nearest neighbour, bilinear, or bicubic.
A homography maps any plane to any other plane, which is what lets you rectify a photographed document to a flat rectangle. Interpolation choice is a real quality decision: nearest neighbour preserves hard labels in masks, bilinear is right for photographs.
Uses: cv2.warpAffine · cv2.warpPerspective · cv2.findHomography
Compression & artefacts
JPEG divides the image into blocks, applies a discrete cosine transform, and quantises high-frequency coefficients — which is where the information is thrown away. PNG is lossless; WebP and AVIF improve on both.
Worth understanding because compression artefacts become training signal. A model trained only on high-quality JPEGs frequently degrades on heavily compressed real-world uploads, and blocking artefacts can be mistaken for texture.
Uses: cv2.imencode · PIL.Image.save · imageio
Advanced
Feature detectors & descriptors
Corners and blobs are locations identifiable across viewpoints. Harris detects corners; SIFT and ORB add scale and rotation invariance plus a descriptor vector for matching between images.
These carried vision for two decades and remain in use where learned features are unnecessary or unaffordable — SLAM, panorama stitching, and registration all still run on them.
Uses: cv2.SIFT_create · cv2.ORB_create · cv2.BFMatcher
Registration & stitching
Aligning multiple images of the same scene: detect features, match them, estimate a transform robustly with RANSAC to reject bad matches, then warp and blend.
RANSAC is the key idea and generalises well beyond vision — fit a model on random minimal subsets, keep the hypothesis with the most inliers, and let outliers be ignored rather than averaged in.
Uses: cv2.findHomography (RANSAC) · cv2.Stitcher
Advanced denoising & restoration
Non-local means averages similar patches from anywhere in the image rather than only nearby pixels; wavelet methods threshold coefficients in a multi-scale basis; deconvolution attempts to invert a known blur.
The insight behind non-local means — that natural images are self-similar, so the best evidence about a patch is other patches like it — later reappears in self-attention.
Uses: cv2.fastNlMeansDenoising · skimage.restoration · PyWavelets
Colour constancy & white balance
Recovering how a scene would look under neutral illumination. Grey-world assumes the average scene is achromatic; more careful methods estimate the illuminant explicitly.
It matters for datasets because illumination is a confound: a classifier can learn indoor tungsten lighting instead of the object, and then fail outdoors.
Uses: cv2.xphoto.createSimpleWB · colour-science
2 · Computer Vision
Libraries & packagestorch torchvision timm ultralytics transformers albumentations pycocotools opencv-python
Recovering meaning and geometry from images — what is present, where it is, and how the scene is arranged in three dimensions.
Foundations
The core task taxonomy
Classification assigns a label to the whole image. Detection adds localisation with bounding boxes. Semantic segmentation labels every pixel by class; instance segmentation separates individual objects; panoptic segmentation does both at once.
Choosing the weakest task that solves your problem is a real cost decision — annotation effort rises steeply from image labels to boxes to pixel masks.
Uses: torchvision.datasets · pycocotools
CNN building blocks
Convolutional layers apply learned filters, stride controls downsampling, padding preserves spatial size, and pooling aggregates locally. Stacking them grows the receptive field — how much of the input one output unit can see.
Receptive field is the concept to hold onto: if it is smaller than the object, the network structurally cannot see the whole thing, no matter how much you train it.
Uses: torch.nn.Conv2d · torch.nn.MaxPool2d
Backbones & transfer learning
A backbone is a feature extractor — ResNet, EfficientNet, ViT — pretrained on a large dataset and reused. You attach a task-specific head and fine-tune.
This is the default starting point for almost all applied vision. Early layers learn edges and textures that transfer across essentially every visual domain, so training from scratch is rarely justified below very large data scales.
Uses: timm.create_model · torchvision.models
Data augmentation
Synthetic variation applied during training — flips, crops, rotations, colour jitter, and stronger schemes like Mixup, CutMix, and RandAugment.
It encodes the invariances you want. The discipline is that augmentation must preserve the label: horizontal flips are fine for cats and wrong for text or for distinguishing left from right hands.
Uses: albumentations · torchvision.transforms.v2
IoU, NMS and detection basics
Intersection over Union measures box overlap. Detectors emit many overlapping candidates, so non-maximum suppression keeps the highest-confidence box and discards others above an IoU threshold.
NMS is a frequent source of subtle bugs: too aggressive and genuinely overlapping objects get deleted, too permissive and duplicates survive into the output.
Uses: torchvision.ops.nms · torchvision.ops.box_iou
Evaluation metrics
mAP averages precision over recall levels and IoU thresholds for detection; Dice and IoU score segmentation masks; classification uses precision, recall, and F1.
Metric choice encodes what you care about. A pedestrian detector should be scored on recall at high precision, because a missed pedestrian and a false box are not equally costly — and mAP hides that entirely.
Uses: torchmetrics · pycocotools.cocoeval
Core
Detection architectures
Two-stage detectors (R-CNN → Faster R-CNN) propose regions then classify them, favouring accuracy. One-stage detectors (YOLO, SSD, RetinaNet) predict directly over a grid in a single pass, favouring speed. Anchor-free methods (FCOS, CenterNet) drop preset box priors entirely.
Focal loss was the pivotal idea for one-stage models: background overwhelms foreground by orders of magnitude, and down-weighting easy negatives is what closed the accuracy gap.
Uses: ultralytics.YOLO · torchvision.models.detection
Segmentation architectures
FCN made segmentation fully convolutional. U-Net added skip connections between encoder and decoder, restoring the spatial detail that downsampling destroys — which is why it dominates medical imaging. Mask R-CNN adds a mask head to detection; DeepLab uses atrous convolution to widen receptive field without losing resolution.
The recurring tension is resolution against context: you must downsample to see broadly, and you must retain detail to place boundaries precisely.
Uses: segmentation_models_pytorch · torchvision maskrcnn
Vision transformers
ViT splits an image into patches, embeds them as tokens, and applies a standard transformer. Without convolution's built-in locality bias, it needs more data or stronger augmentation — but scales better when that data exists.
DETR reframed detection as set prediction with bipartite matching, removing anchors and NMS from the pipeline entirely and making detection end-to-end for the first time.
Uses: timm · transformers.ViTModel · DetrForObjectDetection
Object tracking
Maintaining identity across frames. SORT combines a Kalman filter for motion with Hungarian matching on IoU; DeepSORT adds an appearance embedding so identities survive occlusion.
The hard part is not detection but association — deciding whether the box in this frame is the same object as the box in the last one, through occlusion, crossing paths, and missed detections.
Uses: ultralytics track · filterpy (Kalman) · scipy.optimize.linear_sum_assignment
Pose estimation & keypoints
Locating structured landmarks — human joints, facial points, object keypoints. Top-down approaches detect a person then find joints within the crop; bottom-up detect all joints then group them.
Heatmap regression rather than direct coordinate regression is the standard trick: predicting a spatial probability map per keypoint is far easier to learn than predicting numbers.
Uses: ultralytics pose · mediapipe · torchvision keypointrcnn
Camera models & calibration
The pinhole model relates 3D points to image pixels through intrinsics (focal length, principal point) and extrinsics (rotation, translation). Real lenses add distortion that must be estimated and corrected.
Nothing metric is possible without this. Any claim about real-world size, distance, or position depends on knowing the camera parameters — uncalibrated images support relative reasoning only.
Uses: cv2.calibrateCamera · cv2.undistort
Advanced
Stereo & depth estimation
Two cameras a known distance apart produce disparity between corresponding points, which converts directly to depth. Monocular depth estimation instead learns the mapping from a single image using learned priors.
Monocular depth is fundamentally ambiguous — a small near object and a large far one project identically — so learned models produce plausible relative depth rather than metric truth.
Uses: cv2.StereoSGBM_create · transformers DPT
Multi-view geometry & SfM
Epipolar geometry constrains where a point in one view can appear in another, encoded in the fundamental and essential matrices. Structure from Motion recovers both 3D structure and camera poses from an unordered image collection; SLAM does it online.
Bundle adjustment — jointly refining all points and all poses to minimise reprojection error — is the optimisation at the centre of both.
Uses: COLMAP · pycolmap · cv2.findEssentialMat
Neural rendering: NeRF & Gaussian splatting
NeRF represents a scene as a neural field mapping position and viewing direction to colour and density, rendered by volumetric ray marching. 3D Gaussian splatting instead represents the scene as explicit anisotropic Gaussians and rasterises them.
Splatting largely displaced NeRF for practical use by making rendering real-time while keeping quality, trading an implicit representation for an explicit, editable one.
Uses: nerfstudio · gsplat
Self-supervised visual learning
Learning representations without labels. Contrastive methods (SimCLR, MoCo) pull augmented views of the same image together and push different images apart; masked autoencoders reconstruct hidden patches; DINO uses self-distillation.
The payoff is that labels become needed only for fine-tuning, and DINO features show emergent segmentation without ever being trained on masks — evidence the representation captures real structure.
Uses: lightly · transformers Dinov2Model
Open-vocabulary vision & SAM
Traditional detectors recognise a fixed label set. Open-vocabulary models align visual features with text embeddings so any phrase becomes a query. Segment Anything is promptable — a point, box, or mask yields a segmentation without class labels at all.
This separates segmentation from recognition: SAM finds the boundary, a language model decides what the thing is, and the two compose without retraining either.
Uses: segment_anything · transformers OwlViT
Video understanding
Adding time. Approaches range from frame-wise CNNs with temporal pooling, through 3D convolutions and two-stream networks using optical flow, to video transformers with spatiotemporal attention.
The binding constraint is cost: attention over every frame patch is quadratic in sequence length, so nearly every practical system is a compromise on sampling rate and resolution.
Uses: decord · torchvision.io.read_video · transformers VideoMAE
3 · Diffusion Models
Libraries & packagesdiffusers torch transformers accelerate peft controlnet_aux safetensors
Generative vision. How iterative denoising became the dominant approach to image synthesis, and the machinery that made it controllable and fast enough to use.
Foundations
The generative landscape
VAEs encode to a probabilistic latent and decode, giving stable training but blurry samples. GANs pit generator against discriminator, giving sharp samples but unstable training and mode collapse. Normalising flows are exactly invertible but architecturally constrained.
Diffusion won because it trades a single hard problem for many easy ones: instead of learning to generate an image in one step, learn to remove a little noise — a stable regression objective, repeated.
Uses: diffusers · torch
The forward (noising) process
Gaussian noise is added over many timesteps until the image becomes pure noise. This process is fixed, requires no learning, and has a closed form — you can jump to any timestep directly rather than simulating step by step.
That closed form is what makes training practical: sample a random timestep, noise the image once, and train on it.
Uses: diffusers.DDPMScheduler.add_noise
The reverse (denoising) process
Generation runs the process backwards: start from noise and repeatedly remove a little, guided by a network trained to predict the noise present at each step.
The training objective is remarkably plain — predict the noise you added, measured by mean squared error. Nearly all of the apparent complexity in diffusion sits in sampling, not in training.
Uses: diffusers.UNet2DConditionModel
Noise schedules
The schedule sets how much noise is added at each timestep — linear, cosine, or learned. It determines how the difficulty of the denoising task is distributed across the trajectory.
Cosine schedules improved results by not destroying the image too quickly at the start, leaving more of the trajectory spent on the steps that actually carry structure.
Uses: DDPMScheduler(beta_schedule=...)
The denoiser network
Usually a U-Net — encoder, decoder, skip connections — conditioned on the timestep so one network handles every noise level. Attention layers at lower resolutions supply global coherence. Newer systems increasingly use transformers (DiT) instead.
Timestep conditioning is what allows a single set of weights to serve as a thousand different denoisers.
Uses: UNet2DConditionModel · DiTTransformer2DModel
Core
DDPM and DDIM sampling
DDPM sampling is stochastic and originally needed a thousand steps. DDIM reformulates it as a deterministic process that can skip steps, producing comparable images in twenty to fifty.
Determinism also gives reproducibility and meaningful latent interpolation — the same seed yields the same image, and paths between seeds become traversable.
Uses: DDIMScheduler · DDPMScheduler
Classifier-free guidance
Train the model both with and without conditioning, then at sampling time extrapolate away from the unconditional prediction toward the conditional one. A guidance scale controls how far.
This single trick is most of why text-to-image works as well as it does. The scale is a direct prompt-adherence against diversity dial: too low and the prompt is ignored, too high and images become oversaturated and stereotyped.
Uses: guidance_scale in any diffusers pipeline
Latent diffusion
Rather than diffusing in pixel space, compress the image with an autoencoder and diffuse in that much smaller latent space, decoding at the end. This is what Stable Diffusion is.
The saving is enormous — a 512×512 image becomes a 64×64 latent, roughly sixty-four times fewer positions — and it is the reason high-resolution generation became feasible on consumer hardware.
Uses: AutoencoderKL · StableDiffusionPipeline
Text conditioning & cross-attention
A text encoder (originally CLIP, later T5-family) embeds the prompt, and cross-attention layers inside the denoiser let each spatial position attend to text tokens.
Text encoder quality is a major determinant of prompt fidelity — several generational jumps in prompt following came from changing the text encoder rather than the diffusion model.
Uses: CLIPTextModel · T5EncoderModel
Samplers & solvers
Sampling is numerical integration of a differential equation, so better solvers need fewer evaluations. Euler, Heun, and DPM-Solver++ trade steps against fidelity.
This is why step count is a user-facing setting: it is literally the number of function evaluations, and the useful range depends on which solver is running.
Uses: DPMSolverMultistepScheduler · EulerDiscreteScheduler
Score-based & SDE formulation
The continuous-time view: diffusion is a stochastic differential equation, and the model learns the score — the gradient of log-density — which points toward higher-probability regions.
This unifies the discrete-step and continuous formulations, and it is the framework in which most modern sampler and distillation work is derived.
Uses: ScoreSdeVeScheduler
Advanced
ControlNet & structural conditioning
ControlNet adds a trainable copy of the encoder that accepts a structural input — edges, depth, pose, segmentation — and injects it into the frozen base model through zero-initialised connections.
Zero initialisation is the trick: the added branch starts as an identity operation, so the base model is not damaged at the beginning of training. It converts an uncontrollable generator into a controllable one without retraining it.
Uses: ControlNetModel · controlnet_aux
Personalisation: LoRA, DreamBooth, textual inversion
Teaching a model a new subject or style from a handful of images. Textual inversion learns a new embedding only; DreamBooth fine-tunes the model against a rare token with a preservation loss; LoRA trains small low-rank adapters.
LoRA dominates in practice because adapters are a few megabytes, compose with each other, and swap at inference — one base model can serve hundreds of styles.
Uses: peft · pipe.load_lora_weights
Inpainting, outpainting and img2img
All three start the reverse process from something other than pure noise. img2img noises an existing image partway and denoises back; inpainting restricts changes to a mask; outpainting extends beyond the original border.
The denoising strength setting is just where in the trajectory you begin — high strength discards more of the original, low strength preserves composition and alters only surface detail.
Uses: StableDiffusionImg2ImgPipeline · ...InpaintPipeline
Distillation & few-step generation
Consistency models, latent consistency models, and adversarial distillation compress the sampling trajectory so a student produces in one to four steps what the teacher needed fifty for.
This is what made real-time and interactive generation possible. The cost is usually some diversity and fine detail — distilled models are noticeably more deterministic.
Uses: LCMScheduler · AutoPipelineForText2Image
Flow matching & rectified flow
Instead of a noising process, learn a velocity field that transports noise to data along near-straight paths. Training is a simple regression onto that velocity.
Straighter trajectories are cheaper to integrate, so fewer steps are needed by construction. Several recent large image and video generators are built on this rather than classical diffusion.
Uses: FlowMatchEulerDiscreteScheduler
Video diffusion
Extending generation across time, using temporal attention or 3D convolutions, often initialised from an image model and fine-tuned on video.
Temporal consistency is the hard part: independently generating frames yields flicker, and the compute cost scales with frame count, which is why most systems generate short clips at limited resolution.
Uses: StableVideoDiffusionPipeline · export_to_video
Evaluating generative models
FID compares feature distributions of real and generated sets; CLIP score measures prompt alignment; human preference remains the reference standard.
All the automatic metrics are weak. FID is sensitive to sample count and preprocessing and rewards distribution matching rather than quality, and a model can score well while producing images nobody likes — which is why leaderboards in this area are dominated by human-preference arenas.
Uses: torchmetrics FID · clip_score
4 · Vision-Language Models
Libraries & packagestransformers open_clip_torch torch Pillow accelerate pytesseract lmms-eval
Models that connect pixels to language — the alignment idea that made zero-shot vision possible, and the architectures that gave language models eyes.
Foundations
The alignment idea
Train an image encoder and a text encoder so that matching image–text pairs land close together in a shared embedding space, and mismatched pairs land far apart.
Once that space exists, classification becomes retrieval: embed the image, embed candidate label phrases, and take the nearest. This is the mechanism behind zero-shot vision.
Uses: open_clip · transformers CLIPModel
CLIP and contrastive pretraining
CLIP trains on hundreds of millions of image–caption pairs with a contrastive objective over a large batch: each image should match its own caption against all the others in the batch as negatives.
Batch size is a first-class hyperparameter here, because it determines how many negatives each example sees. CLIP embeddings became infrastructure — used for retrieval, filtering, guidance, and as the vision encoder inside later VLMs.
Uses: open_clip_torch · CLIPProcessor
The encoder–projector–LLM pattern
The dominant modern architecture: a pretrained vision encoder produces patch features, a projection layer maps them into the language model's embedding space, and the LLM consumes them as if they were tokens.
Its appeal is economy. Both large components are pretrained and often frozen; only the small projector must be trained, so a capable VLM can be built without training either half from scratch.
Uses: transformers AutoModelForVision2Seq
Captioning and VQA
The two canonical tasks — describe this image, and answer a question about it. Both are now generative rather than classification tasks.
Their evaluation is genuinely unresolved. n-gram metrics like CIDEr reward matching reference phrasing, so a correct description written differently scores badly, which is why benchmarks increasingly use multiple-choice or model judges.
Uses: BlipForConditionalGeneration · AutoProcessor
Core
BLIP-2 and the Q-Former
BLIP-2 inserts a lightweight querying transformer between a frozen vision encoder and a frozen LLM. A small set of learned query tokens attends to image features and outputs a fixed-length summary for the language model.
It solves the bridging problem economically: a fixed number of query outputs regardless of image resolution, so the language model's context cost stays bounded.
Uses: Blip2ForConditionalGeneration
LLaVA and visual instruction tuning
LLaVA showed that a simple linear or MLP projection plus instruction-tuning data is enough for strong conversational visual ability, with training data generated by prompting a text-only model with image annotations.
The lesson was that data mattered more than architecture — a minimal connector with good instruction data outperformed more elaborate designs, and this recipe became the default.
Uses: LlavaForConditionalGeneration
Cross-attention architectures
Flamingo interleaves gated cross-attention layers inside a frozen language model, letting text tokens attend to image features at multiple depths rather than only at the input.
It handles interleaved image–text sequences naturally and supports few-shot prompting with images, at the cost of modifying the language model's internals rather than only its input.
Uses: IdeficsForVisionText2Text
Grounding & referring expressions
Connecting language to specific image regions — "the mug behind the laptop" must resolve to coordinates, not just a description. Models output boxes or masks alongside text.
This is what separates a model that describes a scene from one that can act in it, and it is the capability robotics and UI-automation work depends on.
Uses: OwlViTForObjectDetection · GroundingDINO
OCR & document understanding
Reading text in images — signage, receipts, forms, charts, screenshots — and reasoning over layout as well as content.
It is disproportionately resolution-sensitive: small text is destroyed by the downsampling most vision encoders apply, which is why document-capable models invest heavily in high-resolution handling.
Uses: pytesseract · transformers TrOCR · docTR
Advanced
Hallucination in VLMs
Describing objects, attributes, or relationships that are not present. Object hallucination is measurable with benchmarks like POPE, which simply ask whether a given object appears.
The causes are structural: language priors overwhelm weak visual evidence, so the model completes a plausible scene rather than reporting the observed one. Mitigations include stronger visual grounding, contrastive decoding, and training on negatives.
Uses: POPE benchmark · lmms-eval
High-resolution and token cost
Vision encoders were trained at modest fixed resolutions, but real tasks need detail. Solutions include tiling the image into crops, dynamic aspect-ratio handling, and multi-scale encoding.
Every approach trades against context: more visual tokens means better detail and fewer tokens left for reasoning, and a single high-resolution image can consume thousands of tokens.
Uses: AutoProcessor(size=...) · dynamic tiling
Visual token compression
Reducing how many tokens an image occupies through pruning uninformative patches, merging similar ones, or learned resamplers.
An interesting extreme is optical compression, where text is rendered as an image because visual tokens can carry it more densely than text tokens — which inverts the usual assumption about which modality is expensive.
Uses: transformers resamplers · Perceiver
Video VLMs
Extending to video means choosing frames, since encoding every frame is infeasible. Strategies include uniform sampling, keyframe selection, and temporal pooling of features.
Sampling is where most video VLMs actually fail: an event lasting half a second is simply absent if it falls between sampled frames, and no amount of reasoning recovers it.
Uses: decord · VideoLlavaForConditionalGeneration
Spatial and 3D-aware reasoning
Answering questions about relative position, distance, size, and occlusion. Standard VLMs are notably weak here, since 2-D patch features encode little explicit geometry.
Recent work augments VLMs with reconstructed 3D structure from monocular video, moving from models that describe a scene toward models that hold a geometric representation of it.
Evaluation & contamination
Benchmarks such as MMMU, MMBench, and MathVista probe reasoning rather than recognition, usually as multiple choice for automatic scoring.
Two caveats. Multiple choice is gameable — some models score above chance from text alone, without the image. And contamination is pervasive, since benchmark images and questions are on the public web and end up in pretraining data.
Uses: lmms-eval · MMMU
5 · Multimodal Language Modelling
Libraries & packagestransformers torch torchaudio decord datasets accelerate lerobot
Beyond vision alone — architectures that take in and produce several modalities, and the problems that only appear once you stop treating text as the centre.
Foundations
What multimodal actually means
More than one modality — text, image, audio, video, and increasingly action — handled by one model. Some accept several inputs but emit only text; genuinely multimodal systems both understand and generate across modalities.
The distinction matters when reading claims: many "multimodal" models are multimodal in and text-only out.
Tokenising other modalities
Transformers consume sequences of tokens, so every modality must become one. Images become patches, audio becomes spectrogram frames or learned acoustic codes, video becomes spatiotemporal patches.
Discrete tokenisers such as VQ-VAE make generation as straightforward as next-token prediction, which is why any-to-any models tend to adopt them.
Uses: torchaudio · transformers AutoFeatureExtractor
Fusion strategies
Early fusion combines modalities at the input so all layers see everything. Late fusion encodes separately and merges near the output. Intermediate fusion interleaves cross-attention at several depths.
Early fusion allows the richest interaction and costs the most; late fusion is cheap and modular but can only combine already-formed representations.
Uses: torch.nn.MultiheadAttention
Core
Native multimodal pretraining
Training on mixed-modality data from the start, rather than attaching a vision encoder to a finished language model.
It produces better cross-modal integration because representations co-develop, at the cost of far more expensive training and the inability to reuse an existing language model.
Uses: transformers · accelerate
Interleaved data
Training on documents where images and text alternate naturally — web pages, papers, tutorials — rather than isolated caption pairs.
This is what teaches a model that an image can refer to text before it and be referred to by text after it. It is also the source of in-context multimodal few-shot ability.
Uses: datasets · webdataset
Shared versus specialised encoders
Either train modality-specific encoders that project into a common space, or push a single architecture to handle raw inputs from several modalities directly.
Specialised encoders exploit known structure and reuse pretrained weights; shared architectures are simpler and scale better, and the field has been drifting toward them as data and compute grow.
Cross-modal attention
The mechanism that lets one modality condition another — text tokens attending to image patches, audio attending to text.
Where and how often it appears is the main architectural decision in multimodal design, and it determines both integration quality and compute cost.
Uses: torch.nn.MultiheadAttention
Advanced
Any-to-any generation
One model that both understands and generates across modalities — text to image, image to audio, video to text — usually by unifying everything into discrete tokens and predicting them autoregressively.
It removes the pipeline of separate specialist models and allows genuine cross-modal composition, though per-modality quality has generally trailed dedicated systems.
Uses: transformers · VQ tokenizers
Modality imbalance and collapse
When one modality is easier or more abundant, the model leans on it and underuses the others — a VQA model that answers from the question alone, ignoring the image.
It inflates benchmark scores while the model is not actually looking. The diagnostic is simple and underused: withhold a modality and see whether performance drops. If it barely moves, it was not being used.
Uses: ablation harness · lmms-eval
Long-context multimodal
Reasoning over hours of video, long documents with figures, or extended interaction histories, where token counts run to hundreds of thousands.
Compression is unavoidable at this scale, and the open question is what to discard. Uniform sampling is simple and loses precisely the brief events that often matter most.
Uses: decord · frame samplers
Action as a modality: VLA models
Vision-language-action models add robot actions as an output modality, mapping observations and instructions directly to control commands.
Action differs from other modalities in a way that matters: it is closed-loop and consequential. Errors change the world and compound, so there is no equivalent of regenerating a bad caption.
Uses: lerobot · transformers
World models
Learned models that predict how a scene evolves, allowing an agent to simulate outcomes before acting. Increasingly used inside training loops rather than only for evaluation.
Evaluating them is unresolved: visual fidelity is easy to measure and largely irrelevant, while what matters is whether the predicted dynamics support good decisions.
Cross-modal safety and alignment
Safety work developed for text does not transfer cleanly. Instructions can be embedded in an image, harmful content can be split across modalities, and refusal behaviour trained on text often fails when the same request arrives visually.
The general lesson is that every added modality is an added attack surface, and guardrails have to be evaluated per modality and per combination rather than assumed to generalise.
6 · Generative Vision — Autoencoders & GANs
Libraries & packagestorch.nn torch.distributions torchmetrics.image diffusers.AutoencoderKL kornia timm lpips
The generative half of vision that predates diffusion and still underpins it. Autoencoders give you a latent space; GANs give you a way to make samples from one look real. Latent diffusion is both of these stacked, so this subdomain is a prerequisite for § 3 rather than an alternative to it.
Foundations
The autoencoder bottleneck
An encoder compresses input to a low-dimensional code, a decoder reconstructs the input from it, and the loss is reconstruction error against the input itself. No labels are involved, which makes it the simplest form of representation learning.
The bottleneck is the entire mechanism. Given enough latent capacity the network learns the identity function and generalises nothing — so the constraint, not the architecture, is what forces useful structure to be discovered.
Uses: torch.nn.Sequential · torch.nn.MSELoss
Denoising, sparse and contractive variants
Denoising autoencoders corrupt the input and ask for the clean version, which prevents identity-copying and produces features robust to exactly the corruption used. Sparse autoencoders penalise mean activation so only a few units fire per input, yielding parts-based features — and are now central to interpretability work on language models.
Contractive autoencoders penalise the Jacobian of the encoding with respect to the input, so the code changes little under small input perturbations. All three are the same idea: the useful autoencoder is the constrained one.
Uses: torch.autograd.functional.jacobian · torch.nn.L1Loss
Reconstruction losses
Pixel MSE is the default and the reason autoencoder outputs blur — under uncertainty the loss-minimising output is the average of plausible reconstructions. L1 blurs slightly less. Perceptual loss compares activations of a pretrained network rather than pixels, so outputs match in feature space and look sharper.
LPIPS is the standard learned perceptual metric. Combining pixel loss with perceptual loss, and sometimes an adversarial term, is what production autoencoders actually use — including the VAE inside Stable Diffusion.
Uses: lpips · torchmetrics.image.LearnedPerceptualImagePatchSimilarity
Anomaly detection by reconstruction
Train only on normal data; anything the autoencoder reconstructs badly is anomalous. Attractive because it needs no labelled defects, which is usually the situation in inspection and monitoring.
Its weakness is that a sufficiently powerful autoencoder reconstructs anomalies too. Practical systems constrain capacity, use memory banks of normal features, or compare features from a pretrained network rather than pixels.
Uses: anomalib · torch
Core
Variational autoencoders and the ELBO
A VAE encodes to a distribution rather than a point: the encoder outputs a mean and log-variance, a sample is drawn, and the decoder reconstructs from that sample. This is what makes the latent space continuous and sampleable — a plain autoencoder's latent space has holes, so decoding a random point produces nothing meaningful.
Training maximises the evidence lower bound, which is reconstruction likelihood minus the KL divergence between the encoder's posterior and a standard normal prior. The KL term is the regulariser that pulls the latent space into a usable shape.
Uses: torch.distributions.Normal · torch.distributions.kl_divergence
The reparameterisation trick
Sampling is not differentiable, so gradients cannot flow through a random draw. Writing the sample as z = mu + sigma * eps with eps drawn from a fixed standard normal moves the randomness outside the computation graph, leaving mu and sigma differentiable.
It is a small algebraic move with an outsized consequence — it is what made variational inference trainable by backpropagation, and it is the single most commonly asked VAE question.
Uses: torch.randn_like
Posterior collapse and beta-VAE
If the KL term dominates, the encoder outputs the prior regardless of input and the decoder learns to ignore the latent entirely, producing the same average image every time. This is posterior collapse, and it is the characteristic VAE failure.
Mitigations: KL annealing that ramps the weight up during training, free bits that exempt a minimum KL per dimension from the penalty, and weakening the decoder so it cannot succeed alone. Beta-VAE turns the weight into a deliberate knob — higher beta trades reconstruction quality for more disentangled factors.
Uses: torch · custom loss weighting schedules
The GAN objective
A generator maps noise to images; a discriminator classifies real against generated. The discriminator's gradient tells the generator how to become more convincing. At the theoretical optimum the generator matches the data distribution and the discriminator is reduced to guessing.
The original minimax loss saturates when the discriminator wins early, giving the generator no gradient — so in practice the non-saturating form is used, maximising log D(G(z)) rather than minimising log(1 - D(G(z))).
Uses: torch.nn.BCEWithLogitsLoss
WGAN, gradient penalty and hinge loss
When real and generated distributions barely overlap, the Jensen-Shannon objective gives vanishing gradients. WGAN approximates Earth Mover's distance instead, which stays informative — but requires the critic to be Lipschitz-constrained.
Weight clipping enforced that crudely and damaged capacity; WGAN-GP replaced it with a penalty on the gradient norm at interpolated points. Hinge loss is the common modern default, used in BigGAN and StyleGAN-adjacent work, and is simpler than either.
Uses: torch.autograd.grad · torch.nn.functional.relu
DCGAN and architectural conventions
DCGAN established the recipe that made GAN training reproducible: strided convolutions instead of pooling, transposed convolutions for upsampling, batch normalisation in both networks, ReLU in the generator with tanh output, LeakyReLU in the discriminator, and no fully-connected layers.
Much of it has since been revised — transposed convolution is now usually replaced by upsample-then-convolve to avoid checkerboard artefacts — but it remains the baseline any GAN discussion starts from.
Uses: torch.nn.ConvTranspose2d · torch.nn.BatchNorm2d · torch.nn.LeakyReLU
Mode collapse and training instability
Mode collapse is the generator discovering a handful of outputs that reliably fool the discriminator and abandoning the rest of the distribution. It looks like impressive individual samples with almost no diversity across a batch — which is why you always inspect a grid, never a single image.
Causes trace to the generator optimising against a discriminator that has not seen enough variety. Mitigations: minibatch discrimination so the discriminator sees batch statistics, WGAN-GP's better-behaved objective, two-timescale update rules with different learning rates, and spectral normalisation on the discriminator.
Uses: torch.nn.utils.spectral_norm
Conditional generation — cGAN, pix2pix, CycleGAN
A conditional GAN feeds a label or embedding to both networks so generation can be steered. pix2pix does paired image-to-image translation with a U-Net generator, an L1 term for structural fidelity, and a PatchGAN discriminator that judges overlapping local patches rather than the whole image — cheaper and sharper on texture.
CycleGAN removes the need for paired data with a cycle-consistency loss: translating to the other domain and back should return the original. It is the standard answer for style transfer when no aligned dataset exists, and its known failure is hiding information imperceptibly in the output to satisfy the cycle.
Uses: torch · kornia · pix2pix / CycleGAN reference implementations
Evaluating generative models
There is no held-out likelihood to report, so evaluation is indirect. Inception Score rewards confident and diverse classifier predictions but never looks at the real data, so it cannot detect a model that generates convincing images from the wrong distribution.
Fréchet Inception Distance fits Gaussians to Inception features of real and generated sets and measures the distance between them — lower is better, and it is the standard. Its caveats matter: it is biased by sample count, depends on the feature extractor, and is blind to memorisation of training images. Precision and recall for generative models separate fidelity from coverage, which FID conflates into one number.
Uses: torchmetrics.image.fid.FrechetInceptionDistance · torchmetrics.image.inception · clean-fid
Advanced
StyleGAN and disentangled latent spaces
StyleGAN's key move is a mapping network from the sampled z to an intermediate w space, which need not be Gaussian and so can un-warp the entangled factors that z forces together. w then modulates each layer through AdaIN, giving scale-specific control — coarse layers govern pose and shape, fine layers govern colour and texture.
StyleGAN2 traced the characteristic droplet artefacts to AdaIN's normalisation and replaced it with weight demodulation. The extended W+ space, with a separate vector per layer, is what GAN inversion and editing methods actually operate in.
Uses: stylegan2-ada-pytorch · torch
GAN inversion and latent editing
Editing a real photograph with a GAN requires first finding the latent code that reproduces it — inversion, done by optimisation, by a trained encoder, or by a hybrid of the two. There is a persistent tension between reconstruction accuracy and editability: codes that reconstruct perfectly often sit outside the well-behaved region of the latent space and edit badly.
Once inverted, editing means moving along a direction in latent space. Those directions are found supervised with attribute classifiers, or unsupervised through PCA of sampled codes (GANSpace) and closed-form factorisation (SeFa).
Uses: stylegan2-ada-pytorch · lpips · encoder4editing
Training on limited data
With a few thousand images the discriminator memorises the training set, stops providing useful gradient, and the generator degenerates. Naive augmentation makes it worse — the generator learns to reproduce the augmentation artefacts.
Adaptive discriminator augmentation applies augmentations to both real and fake inputs with a probability tuned automatically from the discriminator's overfitting signal, and the augmentations remain invisible in the generator's output. It made high-quality GAN training on small datasets practical.
Uses: stylegan2-ada-pytorch · kornia.augmentation
VQ-VAE and discrete latents
Instead of a continuous code, the encoder output is snapped to the nearest entry in a learned codebook. The gradient is passed through the non-differentiable lookup with a straight-through estimator, and a commitment loss keeps encoder outputs near their chosen codes.
Discretisation sidesteps posterior collapse and, more importantly, turns an image into a sequence of tokens — which is what lets an autoregressive transformer model images the way it models text. This is the mechanism behind DALL·E 1 and behind image tokenisation in multimodal LLMs.
Uses: vector-quantize-pytorch · torch
Where GANs still win over diffusion
Diffusion surpassed GANs on sample quality, diversity, and controllability for text-to-image, and largely displaced them there. But a GAN generates in one forward pass while diffusion needs tens — so on latency-bound and on-device work the trade-off is not settled.
Super-resolution, face restoration, real-time style transfer, and interactive editing still commonly use GANs or GAN-distilled diffusion models. The honest position is that diffusion is the default for quality and GANs for speed, with distillation steadily narrowing the gap.
Uses: diffusers · basicsr · gfpgan
7 · Animation, Audio & Real-Time 3D
Libraries & packagestorchaudio librosa mediapipe smplx open3d pytorch3d trimesh nerfstudio transformers
Vision output that has to drive something interactive — a character, a voice, a scene rendered at frame rate. The modelling here is mostly familiar; what is unfamiliar is that the deadline is 16 milliseconds and a human perceptual system is the judge.
Foundations
Rigs, skinning and blendshapes
A rig is a hierarchy of joints. Skinning binds each mesh vertex to one or more joints with weights, so moving a joint deforms the surface — linear blend skinning is the standard, with its familiar collapsing artefact at strongly bent joints.
Facial animation normally uses blendshapes instead: a set of sculpted target expressions that are linearly combined by coefficients. This is the interface most ML-driven animation targets, because predicting fifty-odd coefficients per frame is a tractable regression problem.
Uses: trimesh · smplx · ARKit blendshape coefficients
Audio as a spectrogram
The short-time Fourier transform slices a waveform into overlapping windows and takes the frequency content of each, producing a time-frequency image. Mapping frequency onto the mel scale — which is roughly logarithmic, matching human pitch perception — gives the mel spectrogram that nearly all audio models consume.
The consequence worth internalising: once audio is a mel spectrogram it is a 2D array, so convolutional and transformer architectures from vision apply unchanged. Window size sets the trade-off between time and frequency resolution, and that choice matters more than most architecture decisions.
Uses: torchaudio.transforms.MelSpectrogram · librosa.feature.melspectrogram
2D pose estimation
Locating body or hand joints in the image plane. Top-down pipelines detect a person then find joints inside the crop, which is accurate but scales with the number of people; bottom-up pipelines find all joints then group them into individuals, which is constant-time but harder.
Heatmap regression — predicting a spatial probability map per joint rather than coordinates directly — is the standard trick, because a spatial map is a far easier target to learn than raw numbers.
Uses: mediapipe · mmpose · ultralytics (pose models)
3D representations
Point clouds are unordered sets, so networks over them must be permutation-invariant — PointNet's shared MLP plus max-pooling was the founding solution. Voxels are regular and convolvable but cost memory cubically. Meshes render efficiently but are awkward to predict. Implicit representations — signed distance fields, occupancy networks — are continuous and resolution-free but need querying.
The representation choice constrains everything downstream, and picking one for a stated use case is a standard interview question rather than a detail.
Uses: open3d · pytorch3d · trimesh
Core
Motion capture from video
Lifting 2D pose to 3D, or regressing the parameters of a body model such as SMPL directly. Parametric models help because they constrain output to anatomically plausible bodies rather than arbitrary joint positions.
The persistent problems are temporal jitter, foot sliding, and depth ambiguity from a single view. Jitter is not fixed by a better per-frame model — it is fixed by predicting over a temporal window or filtering the sequence, because per-frame independence is the cause.
Uses: smplx · mmpose · pytorch3d
Temporal smoothing and filtering
Any per-frame prediction driving animation needs smoothing or it reads as noise. A Kalman filter models position and velocity and is the classical answer. The One Euro filter is the practical favourite for interactive work because it adapts its cutoff to speed — heavy smoothing when still, light smoothing when moving fast.
The trade-off is always smoothing against latency: every filter that removes jitter also delays response, and in an interactive system users feel the delay.
Uses: filterpy · One Euro filter implementations
Speech representation models
wav2vec 2.0 learns from raw audio by masking latent speech units and solving a contrastive task over quantised targets; HuBERT uses offline clustering to provide masked-prediction targets. Both give representations that fine-tune to strong recognition with a fraction of the labelled data.
Whisper took the opposite route — large-scale weakly-supervised training on diverse audio — and is the practical default for transcription because it degrades gracefully on accents, noise, and code-switching.
Uses: transformers (Wav2Vec2, HuBERT, Whisper) · torchaudio.pipelines
Audio-driven facial animation
Mapping speech to mouth motion, either through explicit phoneme-to-viseme rules or by regressing blendshape coefficients from learned audio features. Learned approaches capture coarticulation — the way a sound's mouth shape is influenced by its neighbours — which rule-based mappings miss.
Wav2Lip and SadTalker are the reference points for video-space generation. The evaluation problem is that lip sync quality is perceptual: humans detect misalignment of a few tens of milliseconds, so any metric that ignores timing is measuring the wrong thing.
Uses: transformers · torchaudio · Wav2Lip / SadTalker reference implementations
Depth and scene reconstruction
Depth arrives from stereo matching on a calibrated pair, from structure-from-motion across many views, from monocular estimators such as MiDaS and Depth Anything, or from active sensors — LiDAR and time-of-flight cameras.
The distinction that matters: monocular depth is relative unless anchored by known scale or sensor data. Treating a monocular depth map as metric is a common and consequential mistake in AR work.
Uses: transformers (Depth Anything) · open3d · cv2.StereoSGBM
Advanced
NeRF and Gaussian splatting
NeRF represents a scene as a function from 3D position and viewing direction to colour and density, rendered by marching rays and integrating. It produces striking view synthesis but was originally slow to both train and render, and it bakes lighting into the representation.
3D Gaussian Splatting instead represents the scene as millions of anisotropic Gaussians rasterised directly to the screen, reaching real-time frame rates at comparable quality. For interactive and AR applications it has largely displaced NeRF, which is a rendering-pipeline argument rather than a modelling one.
Uses: nerfstudio · gsplat · pytorch3d
The AR pipeline end to end
Pose tracking through visual-inertial odometry, fusing camera features with IMU because each covers the other's weakness — the IMU drifts but is fast, vision is stable but slow and fails on blur. Then plane detection and scene meshing, anchoring virtual content into that reconstruction, occlusion so virtual objects can pass behind real ones, and lighting estimation so they are shaded consistently.
Occlusion is the piece that most often looks wrong, and it needs either a depth sensor or a per-frame segmentation mask for people — which is a real-time vision model competing for the same compute as everything else.
Uses: ARKit · RealityKit · open3d
The real-time budget
Sixty frames per second leaves sixteen milliseconds for capture, preprocessing, inference, postprocessing, and rendering combined — and the renderer wants the GPU too. On a handheld device this sits under a thermal ceiling, so sustained performance is lower than a benchmark run suggests.
The practical consequences: measure p95 rather than mean, measure after thermal soak rather than cold, budget end to end rather than model-only, and treat model choice as subordinate to the budget. Most accuracy arguments in interactive vision are settled by latency before they are settled by metrics.
Uses: coremltools · onnxruntime · Instruments / Metal System Trace
Generative animation and video
Video diffusion extends image diffusion with temporal layers or full spatiotemporal attention, and the hard constraint is consistency — a model that generates each frame well but independently produces flicker that is immediately obvious.
Approaches include conditioning on previous frames, latent interpolation along a trajectory, and explicit motion conditioning from pose or depth sequences. Evaluation remains unresolved: per-frame quality metrics reward exactly the models that flicker.
Uses: diffusers (video pipelines) · transformers
Code Snippets
Working PyTorch for the concepts above, grouped by subdomain. Written in torch rather than library one-liners wherever the mechanics are the point — the closed-form forward diffusion, the contrastive loss, morphology as pooling — because those are the parts worth being able to write from memory.
1 · Image Processing — in PyTorch
Load an image as a tensor
import torch, torchvision
from torchvision.io import read_image
import torchvision.transforms.v2.functional as TF
img = read_image("in.jpg") # uint8, [C,H,W], 0-255
x = img.float().div(255).unsqueeze(0) # float32, [1,C,H,W], 0-1
x = x.to("cuda") # everything below runs batched on GPU
print(x.shape, x.dtype, x.min().item(), x.max().item())Colour conversion (matrix form)
# RGB -> grayscale using luminance weights, as a 1x1 convolution
w = torch.tensor([0.299, 0.587, 0.114], device=x.device).view(1, 3, 1, 1)
gray = (x * w).sum(1, keepdim=True) # [1,1,H,W]Convolution with your own kernel
import torch.nn.functional as F
sharpen = torch.tensor([[0., -1., 0.],
[-1., 5., -1.],
[0., -1., 0.]], device=x.device)
k = sharpen.view(1, 1, 3, 3).repeat(x.shape[1], 1, 1, 1)
out = F.conv2d(x, k, padding=1, groups=x.shape[1]) # per-channel (depthwise)Separable Gaussian blur
def gaussian_kernel1d(sigma, ks):
t = torch.arange(ks) - ks // 2
g = torch.exp(-(t ** 2) / (2 * sigma ** 2))
return (g / g.sum()).float()
g = gaussian_kernel1d(1.5, 9).to(x.device)
C = x.shape[1]
x = F.conv2d(x, g.view(1, 1, 1, -1).repeat(C, 1, 1, 1), padding=(0, 4), groups=C)
x = F.conv2d(x, g.view(1, 1, -1, 1).repeat(C, 1, 1, 1), padding=(4, 0), groups=C)
# two 1-D passes instead of one 2-D: O(k) per pixel rather than O(k^2)Sobel edges
sx = torch.tensor([[-1., 0., 1.], [-2., 0., 2.], [-1., 0., 1.]], device=x.device)
sy = sx.T.contiguous()
gx = F.conv2d(gray, sx.view(1, 1, 3, 3), padding=1)
gy = F.conv2d(gray, sy.view(1, 1, 3, 3), padding=1)
mag = torch.sqrt(gx ** 2 + gy ** 2)
ang = torch.atan2(gy, gx)Histogram and equalization
hist = torch.histc(gray * 255, bins=256, min=0, max=255)
cdf = hist.cumsum(0)
cdf = (cdf - cdf.min()) / (cdf.max() - cdf.min()) # normalise to 0-1
eq = cdf[(gray * 255).long().clamp(0, 255)] # LUT via indexingMorphology as pooling
# dilation is a max filter; erosion is a min filter (= -max of the negative)
def dilate(m, k=3): return F.max_pool2d(m, k, stride=1, padding=k // 2)
def erode(m, k=3): return -F.max_pool2d(-m, k, stride=1, padding=k // 2)
mask = (gray > 0.5).float()
opened = dilate(erode(mask)) # remove specks
closed = erode(dilate(opened)) # fill holesImage pyramid
pyr, level = [x], x
for _ in range(3):
level = F.avg_pool2d(level, 2) # blur + subsample
pyr.append(level)
up = F.interpolate(pyr[-1], scale_factor=2, mode="bilinear", align_corners=False)FFT and frequency filtering
f = torch.fft.fftshift(torch.fft.fft2(gray))
H, W = gray.shape[-2:]
yy, xx = torch.meshgrid(torch.arange(H), torch.arange(W), indexing="ij")
r = ((yy - H // 2) ** 2 + (xx - W // 2) ** 2).sqrt().to(f.device)
low = torch.fft.ifft2(torch.fft.ifftshift(f * (r < 30))).abs() # low-passGeometric warp with grid_sample
theta = torch.tensor([[[1., 0., 0.2], # affine: translate + scale
[0., 1., 0.0]]], device=x.device)
grid = F.affine_grid(theta, x.shape, align_corners=False)
warped = F.grid_sample(x, grid, mode="bilinear", align_corners=False)
# grid_sample is differentiable, so warps can sit inside a training graphBatched preprocessing on GPU
from torchvision.transforms import v2
tf = v2.Compose([v2.ToDtype(torch.float32, scale=True),
v2.Resize((256, 256), antialias=True),
v2.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
batch = tf(torch.stack([read_image(p) for p in paths]).cuda()) # whole batch at once2 · Computer Vision — in PyTorch
Dataset and DataLoader
from torch.utils.data import Dataset, DataLoader
class ImageSet(Dataset):
def __init__(self, paths, labels, tf): self.p, self.y, self.tf = paths, labels, tf
def __len__(self): return len(self.p)
def __getitem__(self, i): return self.tf(read_image(self.p[i])), self.y[i]
dl = DataLoader(ImageSet(paths, labels, tf), batch_size=32, shuffle=True,
num_workers=8, pin_memory=True, persistent_workers=True)Transfer learning: freeze and replace the head
import torchvision.models as M
net = M.resnet50(weights=M.ResNet50_Weights.IMAGENET1K_V2)
for p in net.parameters():
p.requires_grad = False
net.fc = torch.nn.Linear(net.fc.in_features, num_classes) # new head trains
net = net.cuda()Training loop with mixed precision
opt = torch.optim.AdamW([p for p in net.parameters() if p.requires_grad], lr=3e-4)
scaler = torch.amp.GradScaler("cuda")
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)
for ep in range(epochs):
net.train()
for xb, yb in dl:
xb, yb = xb.cuda(non_blocking=True), yb.cuda(non_blocking=True)
opt.zero_grad(set_to_none=True)
with torch.autocast("cuda", dtype=torch.bfloat16):
loss = F.cross_entropy(net(xb), yb)
scaler.scale(loss).backward()
scaler.step(opt); scaler.update()
sched.step()IoU from scratch
def box_iou(a, b): # a:[N,4], b:[M,4] as x1,y1,x2,y2
area_a = (a[:, 2] - a[:, 0]) * (a[:, 3] - a[:, 1])
area_b = (b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1])
lt = torch.max(a[:, None, :2], b[None, :, :2])
rb = torch.min(a[:, None, 2:], b[None, :, 2:])
inter = (rb - lt).clamp(min=0).prod(dim=2)
return inter / (area_a[:, None] + area_b[None, :] - inter)NMS and detection post-processing
from torchvision.ops import nms, batched_nms
keep = nms(boxes, scores, iou_threshold=0.5) # single class
keep = batched_nms(boxes, scores, class_ids, 0.5) # per class, no cross-suppressionDice loss for segmentation
def dice_loss(logits, target, eps=1.0):
prob = torch.sigmoid(logits)
num = 2 * (prob * target).sum(dim=(2, 3)) + eps
den = prob.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) + eps
return 1 - (num / den).mean()
loss = 0.5 * F.binary_cross_entropy_with_logits(logits, target) + 0.5 * dice_loss(logits, target)ViT patch embedding
class PatchEmbed(torch.nn.Module):
def __init__(self, dim=768, patch=16, in_ch=3):
super().__init__()
self.proj = torch.nn.Conv2d(in_ch, dim, kernel_size=patch, stride=patch)
def forward(self, x): # [B,3,224,224]
return self.proj(x).flatten(2).transpose(1, 2) # [B,196,768] tokens
# a strided convolution IS the patch split plus linear projectionInference correctly
net.eval()
with torch.inference_mode(): # no autograd, no graph
logits = net(batch.cuda())
probs = logits.softmax(dim=1)
# forgetting .eval() leaves BatchNorm/Dropout in training mode — a classic silent bug3 · Diffusion Models — in PyTorch
The noise schedule
T = 1000
betas = torch.linspace(1e-4, 0.02, T) # linear schedule
alphas = 1.0 - betas
abar = torch.cumprod(alphas, dim=0) # alpha-bar_tForward process in closed form
def q_sample(x0, t, noise):
a = abar[t].view(-1, 1, 1, 1)
return a.sqrt() * x0 + (1 - a).sqrt() * noise
# no loop over timesteps — jump straight to any tThe training step (this is the whole objective)
t = torch.randint(0, T, (x0.size(0),), device=x0.device)
noise = torch.randn_like(x0)
xt = q_sample(x0, t, noise)
pred = unet(xt, t, cond) # predict the noise that was added
loss = F.mse_loss(pred, noise)
loss.backward()Sinusoidal timestep embedding
def timestep_embedding(t, dim):
half = dim // 2
freqs = torch.exp(-torch.arange(half, device=t.device) * (torch.log(torch.tensor(10000.0)) / half))
args = t[:, None].float() * freqs[None]
return torch.cat([args.cos(), args.sin()], dim=-1)
# one network serves every noise level because t is fed in as a conditioning vectorDDPM sampling loop
@torch.no_grad()
def ddpm_sample(shape):
x = torch.randn(shape, device=dev)
for i in reversed(range(T)):
t = torch.full((shape[0],), i, device=dev, dtype=torch.long)
eps = unet(x, t, cond)
a, ab = alphas[i], abar[i]
mean = (x - (1 - a) / (1 - ab).sqrt() * eps) / a.sqrt()
x = mean + (betas[i].sqrt() * torch.randn_like(x) if i > 0 else 0)
return xDDIM: deterministic, skippable steps
@torch.no_grad()
def ddim_sample(shape, steps=50):
ts = torch.linspace(T - 1, 0, steps).long()
x = torch.randn(shape, device=dev)
for i, j in zip(ts[:-1], ts[1:]):
eps = unet(x, i.repeat(shape[0]).to(dev), cond)
x0 = (x - (1 - abar[i]).sqrt() * eps) / abar[i].sqrt()
x = abar[j].sqrt() * x0 + (1 - abar[j]).sqrt() * eps # no noise added
return xClassifier-free guidance
eps_uncond = unet(x, t, null_cond)
eps_cond = unet(x, t, cond)
eps = eps_uncond + guidance_scale * (eps_cond - eps_uncond)
# guidance_scale = 1 is plain conditional; higher pushes harder toward the promptTraining with CFG dropout
mask = (torch.rand(cond.size(0), device=cond.device) < 0.1) # drop 10%
cond = torch.where(mask[:, None], null_cond, cond)
# the same network learns conditional and unconditional prediction4 · Vision-Language Models — in PyTorch
CLIP contrastive loss (symmetric InfoNCE)
def clip_loss(img_emb, txt_emb, logit_scale):
img = F.normalize(img_emb, dim=-1)
txt = F.normalize(txt_emb, dim=-1)
logits = logit_scale.exp() * img @ txt.T # [B,B]
labels = torch.arange(len(logits), device=logits.device)
return 0.5 * (F.cross_entropy(logits, labels) + # image -> text
F.cross_entropy(logits.T, labels)) # text -> image
# every other item in the batch is a negative, so batch size is a real hyperparameterZero-shot classification
with torch.inference_mode():
i = F.normalize(image_encoder(pixels), dim=-1)
t = F.normalize(text_encoder(tokenised_labels), dim=-1)
probs = (100.0 * i @ t.T).softmax(dim=-1)
# classification reduced to nearest-neighbour retrieval in a shared spaceThe projector that gives an LLM eyes
class Projector(torch.nn.Module):
def __init__(self, vision_dim=1024, llm_dim=4096):
super().__init__()
self.net = torch.nn.Sequential(
torch.nn.Linear(vision_dim, llm_dim),
torch.nn.GELU(),
torch.nn.Linear(llm_dim, llm_dim))
def forward(self, patches): # [B, N_patches, vision_dim]
return self.net(patches) # [B, N_patches, llm_dim] -> used as tokensTrain only the connector
for p in vision_encoder.parameters(): p.requires_grad = False
for p in language_model.parameters(): p.requires_grad = False
opt = torch.optim.AdamW(projector.parameters(), lr=1e-3)
vis = projector(vision_encoder(pixels)) # visual tokens
embeds = torch.cat([vis, language_model.get_input_embeddings()(text_ids)], dim=1)
loss = language_model(inputs_embeds=embeds, labels=labels).lossMasking image tokens out of the loss
labels = text_ids.clone()
labels[:, :vis.size(1)] = -100 # ignore_index: never predict visual positions
# forgetting this trains the model to 'generate' image tokens and quietly degrades it5 · Multimodal Language Modelling — in PyTorch
A cross-attention block
class CrossAttn(torch.nn.Module):
def __init__(self, dim, heads=8):
super().__init__()
self.attn = torch.nn.MultiheadAttention(dim, heads, batch_first=True)
self.norm = torch.nn.LayerNorm(dim)
self.gate = torch.nn.Parameter(torch.zeros(1)) # start as identity
def forward(self, text, other):
out, _ = self.attn(self.norm(text), other, other)
return text + self.gate.tanh() * out # gated residualEarly vs late fusion
# early: concatenate tokens, every layer sees both
seq = torch.cat([img_tokens, txt_tokens], dim=1)
out = transformer(seq)
# late: encode separately, combine at the end
out = head(torch.cat([img_encoder(img).mean(1), txt_encoder(txt).mean(1)], dim=-1))Modality type embeddings
mod_emb = torch.nn.Embedding(3, dim) # 0=text 1=image 2=audio
types = torch.cat([torch.zeros(Lt), torch.ones(Li), torch.full((La,), 2)]).long()
seq = seq + mod_emb(types.to(seq.device))[None]
# without this the model cannot tell which tokens came from which modalityModality dropout during training
if self.training and torch.rand(1).item() < 0.15:
img_tokens = torch.zeros_like(img_tokens) # force reliance on text sometimes
# improves robustness and reduces collapse onto one modalityThe collapse test
@torch.inference_mode()
def ablate(model, loader):
full = evaluate(model, loader)
blind = evaluate(model, loader, zero_out="image")
return full, blind, full - blind
# a small gap means the image was never really being usedPadding and attention masks across modalities
attn_mask = torch.cat([
torch.ones(B, Li, dtype=torch.bool, device=dev), # images: always present
text_attention_mask.bool(), # text: variable length
], dim=1)
out = model(inputs_embeds=seq, attention_mask=attn_mask)6 · Generative Vision — in PyTorch
A convolutional autoencoder with an explicit bottleneck
import torch, torch.nn as nn
class AutoEncoder(nn.Module):
def __init__(self, latent=128):
super().__init__()
self.enc = nn.Sequential(
nn.Conv2d(3, 32, 4, 2, 1), nn.ReLU(), # 64 -> 32
nn.Conv2d(32, 64, 4, 2, 1), nn.ReLU(), # 32 -> 16
nn.Conv2d(64, 128, 4, 2, 1), nn.ReLU(), # 16 -> 8
nn.Flatten(), nn.Linear(128 * 8 * 8, latent),
)
self.dec = nn.Sequential(
nn.Linear(latent, 128 * 8 * 8), nn.Unflatten(1, (128, 8, 8)),
nn.Upsample(scale_factor=2), nn.Conv2d(128, 64, 3, 1, 1), nn.ReLU(),
nn.Upsample(scale_factor=2), nn.Conv2d(64, 32, 3, 1, 1), nn.ReLU(),
nn.Upsample(scale_factor=2), nn.Conv2d(32, 3, 3, 1, 1), nn.Sigmoid(),
)
def forward(self, x):
return self.dec(self.enc(x))
# upsample-then-convolve rather than ConvTranspose2d: no checkerboard artefactsDenoising objective — corrupt the input, reconstruct the clean target
clean = batch
noisy = clean + 0.2 * torch.randn_like(clean)
loss = nn.functional.mse_loss(model(noisy.clamp(0, 1)), clean)VAE: encode to a distribution, reparameterise, decode
class VAE(nn.Module):
def __init__(self, latent=128):
super().__init__()
self.backbone = nn.Sequential(nn.Flatten(), nn.Linear(3 * 64 * 64, 512), nn.ReLU())
self.to_mu = nn.Linear(512, latent)
self.to_logvar = nn.Linear(512, latent)
self.dec = nn.Sequential(
nn.Linear(latent, 512), nn.ReLU(),
nn.Linear(512, 3 * 64 * 64), nn.Sigmoid(), nn.Unflatten(1, (3, 64, 64)),
)
def encode(self, x):
h = self.backbone(x)
return self.to_mu(h), self.to_logvar(h)
def reparameterise(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std) # randomness lives outside the graph
return mu + std * eps # differentiable in mu and std
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterise(mu, logvar)
return self.dec(z), mu, logvarThe ELBO, with beta as an explicit knob
def elbo_loss(recon, x, mu, logvar, beta=1.0):
rec = nn.functional.mse_loss(recon, x, reduction="sum") / x.size(0)
# closed form KL between N(mu, sigma^2) and N(0, 1)
kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) / x.size(0)
return rec + beta * kld, rec, kld
# watch kld -> 0 during training: that is posterior collapse, not convergenceKL annealing to avoid collapse
def kl_weight(step, warmup=10_000, target=1.0):
return target * min(1.0, step / warmup)One GAN training step, non-saturating loss
bce = nn.BCEWithLogitsLoss()
def gan_step(G, D, real, opt_g, opt_d, z_dim=128):
B, dev = real.size(0), real.device
# --- discriminator ---
z = torch.randn(B, z_dim, device=dev)
fake = G(z)
d_real = D(real)
d_fake = D(fake.detach()) # detach: no generator gradients here
loss_d = bce(d_real, torch.ones_like(d_real)) + \
bce(d_fake, torch.zeros_like(d_fake))
opt_d.zero_grad(); loss_d.backward(); opt_d.step()
# --- generator: non-saturating form ---
d_fake = D(fake)
loss_g = bce(d_fake, torch.ones_like(d_fake)) # maximise log D(G(z))
opt_g.zero_grad(); loss_g.backward(); opt_g.step()
return loss_d.item(), loss_g.item()WGAN-GP: the gradient penalty term
def gradient_penalty(D, real, fake, lam=10.0):
B = real.size(0)
eps = torch.rand(B, 1, 1, 1, device=real.device)
mixed = (eps * real + (1 - eps) * fake).requires_grad_(True)
scores = D(mixed)
grads = torch.autograd.grad(
outputs=scores, inputs=mixed,
grad_outputs=torch.ones_like(scores),
create_graph=True, retain_graph=True,
)[0]
norm = grads.flatten(1).norm(2, dim=1)
return lam * ((norm - 1) ** 2).mean() # push the critic towards 1-LipschitzSpectral normalisation on the discriminator
from torch.nn.utils import spectral_norm
D = nn.Sequential(
spectral_norm(nn.Conv2d(3, 64, 4, 2, 1)), nn.LeakyReLU(0.2),
spectral_norm(nn.Conv2d(64, 128, 4, 2, 1)), nn.LeakyReLU(0.2),
nn.Flatten(), spectral_norm(nn.Linear(128 * 16 * 16, 1)),
)Detecting mode collapse from a batch
@torch.no_grad()
def sample_diversity(G, n=256, z_dim=128, dev="cuda"):
imgs = G(torch.randn(n, z_dim, device=dev)).flatten(1)
imgs = nn.functional.normalize(imgs, dim=1)
sim = imgs @ imgs.T # pairwise cosine similarity
off = sim[~torch.eye(n, dtype=torch.bool, device=dev)]
return off.mean().item() # climbing towards 1.0 = collapsingFID with torchmetrics
from torchmetrics.image.fid import FrechetInceptionDistance
fid = FrechetInceptionDistance(feature=2048).to(dev)
for real in real_loader:
fid.update((real * 255).to(torch.uint8), real=True)
for _ in range(n_batches):
fake = G(torch.randn(B, z_dim, device=dev))
fid.update((fake * 255).to(torch.uint8), real=False)
print(fid.compute()) # sensitive to sample count - keep it fixed across runsVector quantisation with a straight-through estimator
class VectorQuantiser(nn.Module):
def __init__(self, n_codes=512, dim=64, commit=0.25):
super().__init__()
self.codebook = nn.Embedding(n_codes, dim)
self.codebook.weight.data.uniform_(-1 / n_codes, 1 / n_codes)
self.commit = commit
def forward(self, z): # z: [B, dim, H, W]
z_ = z.permute(0, 2, 3, 1).reshape(-1, z.size(1))
d = torch.cdist(z_, self.codebook.weight)
idx = d.argmin(dim=1)
q = self.codebook(idx).view(z.size(0), z.size(2), z.size(3), -1)
q = q.permute(0, 3, 1, 2)
loss = nn.functional.mse_loss(q, z.detach()) + \
self.commit * nn.functional.mse_loss(z, q.detach())
q = z + (q - z).detach() # straight-through: copy the gradient
return q, loss, idx7 · Animation, Audio & Real-Time 3D — in PyTorch
Waveform to mel spectrogram
import torchaudio
wav, sr = torchaudio.load("speech.wav") # [C, T]
wav = torchaudio.functional.resample(wav, sr, 16_000).mean(0, keepdim=True)
mel = torchaudio.transforms.MelSpectrogram(
sample_rate=16_000,
n_fft=400, # 25 ms window at 16 kHz
hop_length=160, # 10 ms hop -> 100 frames per second
n_mels=80,
)(wav)
mel_db = torchaudio.transforms.AmplitudeToDB()(mel) # [1, 80, frames]
# from here it is a 2D array: any vision backbone applies unchangedA conv encoder over audio, reused straight from vision
audio_encoder = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
nn.AdaptiveAvgPool2d((1, None)), # collapse mel bins, keep time
nn.Flatten(2), # [B, 64, frames]
)Regressing blendshape coefficients from audio
class AudioToBlendshapes(nn.Module):
"""Predicts 52 ARKit-style coefficients per frame from audio features."""
def __init__(self, feat_dim=64, n_shapes=52, ctx=8):
super().__init__()
self.temporal = nn.Conv1d(feat_dim, 128, kernel_size=2 * ctx + 1,
padding=ctx) # sees neighbours: coarticulation
self.head = nn.Sequential(nn.ReLU(), nn.Conv1d(128, n_shapes, 1), nn.Sigmoid())
def forward(self, feats): # [B, feat_dim, frames]
return self.head(self.temporal(feats)) # [B, 52, frames] in [0, 1]Penalising jitter explicitly in the loss
def animation_loss(pred, target, w_vel=0.5):
pos = nn.functional.l1_loss(pred, target)
vel = nn.functional.l1_loss(pred.diff(dim=-1), target.diff(dim=-1))
return pos + w_vel * vel
# the velocity term is what stops per-frame prediction reading as noiseOne Euro filter — smoothing that adapts to speed
class OneEuroFilter:
def __init__(self, freq=60.0, min_cutoff=1.0, beta=0.007):
self.freq, self.min_cutoff, self.beta = freq, min_cutoff, beta
self.x_prev = None
self.dx_prev = 0.0
def _alpha(self, cutoff):
tau = 1.0 / (2 * torch.pi * cutoff)
return 1.0 / (1.0 + tau * self.freq)
def __call__(self, x):
if self.x_prev is None:
self.x_prev = x
return x
dx = (x - self.x_prev) * self.freq
a_d = self._alpha(1.0)
self.dx_prev = a_d * dx + (1 - a_d) * self.dx_prev
cutoff = self.min_cutoff + self.beta * self.dx_prev.abs()
a = self._alpha(cutoff) # moving fast -> less smoothing
self.x_prev = a * x + (1 - a) * self.x_prev
return self.x_prevSpeech features from a pretrained model
from transformers import Wav2Vec2Model, Wav2Vec2FeatureExtractor
fe = Wav2Vec2FeatureExtractor.from_pretrained("facebook/wav2vec2-base-960h")
model = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base-960h").eval()
inputs = fe(wav.squeeze().numpy(), sampling_rate=16_000, return_tensors="pt")
with torch.no_grad():
feats = model(**inputs).last_hidden_state # [1, frames, 768] at ~50 HzResampling two modalities onto a common timebase
def align_to(x, n_frames):
"""x: [B, C, T] at some rate -> [B, C, n_frames] at the video rate."""
return nn.functional.interpolate(x, size=n_frames, mode="linear",
align_corners=False)
audio_at_video_rate = align_to(feats.transpose(1, 2), n_frames=video.size(1))Monocular depth, and why it is relative
from transformers import pipeline
depth = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")
rel = torch.tensor(depth(image)["predicted_depth"]) # relative, unitless
def to_metric(rel, known_depth, mask):
"""Anchor with any real measurement - LiDAR sample, known object size."""
scale = known_depth.mean() / rel[mask].mean()
return rel * scaleTiming a model honestly on device
import time
@torch.no_grad()
def benchmark(model, x, warmup=20, runs=200):
for _ in range(warmup): # never time a cold model
model(x)
if x.is_cuda:
torch.cuda.synchronize()
times = []
for _ in range(runs):
t0 = time.perf_counter()
model(x)
if x.is_cuda:
torch.cuda.synchronize()
times.append(time.perf_counter() - t0)
times = torch.tensor(times)
return {"mean_ms": times.mean().item() * 1e3,
"p95_ms": times.quantile(0.95).item() * 1e3}
# report p95: users feel the tail, not the meanExport for on-device inference
import coremltools as ct
model.eval()
traced = torch.jit.trace(model, torch.randn(1, 3, 224, 224))
mlmodel = ct.convert(
traced,
inputs=[ct.ImageType(name="image", shape=(1, 3, 224, 224), scale=1 / 255.0)],
compute_units=ct.ComputeUnit.ALL, # CPU + GPU + Neural Engine
minimum_deployment_target=ct.target.iOS17,
)
mlmodel.save("Model.mlpackage")
# check the conversion report: unsupported ops fall back to CPU silentlyRelated: the Stacks section covers agentic, API, data science, ML, and AWS concepts in the same format.
