02 / the notebook
Blog
Every post here started as a real project — a university report, a research harness, a Rust crate — rebuilt from the original code, backdated to when the work was done, and wherever it earns it, made interactive.
Posts
69 posts, newest first72 wheels: shipping a Rust extension as a Python package when your dependency has no ABI
rtlf links Polars' internal Rust crates, so the compiled .so embeds a hash of Polars' own plan types: one wheel per (OS, arch, Python, polars version), and a script that rewrites its own manifests to get there.
- Interactive
- rust
- python
- pyo3
- maturin
Optimise once, execute forever: smuggling DataFrames into a compiled Polars plan
Polars re-runs its whole query optimizer on every collect(). For one fixed expression scored against a stream of small batches, that is the entire runtime, so I disguised the input as a file that never existed and skipped the optimizer altogether.
- Interactive
- rust
- python
- polars
- query-planning
Complex numbers are the right type for a grid
Advent of Code 2023 day 16 is a beam bouncing off mirrors on a grid: the kind of problem a 301-line N-dimensional vector class was built for the season before, and then quietly abandoned in favour of two lines using Python's built-in complex. A mirror sandbox you can draw your own maze into.
- Interactive
- advent-of-code
- complex-numbers
- grid-algorithms
- python
Dijkstra when you are not allowed to go straight
Advent of Code 2023 day 17 breaks the one assumption every Dijkstra tutorial leans on: that the cheapest way to reach a cell is all you need to remember about it. The fix is not a new algorithm: it is realising the node was never the cell.
- Interactive
- advent-of-code
- dijkstra
- graph-search
- state-space
Folding a cube without hardcoding the folds
Advent of Code 2022 day 22 part 2 gives you a flat net and asks you to walk on it as if it were a cube. Almost everyone hand-tabulates the fourteen edge pairings for their own input. The general version rolls the cube over the net with integer rotation matrices and lets the geometry answer, and it also caught a bug in mine.
- Interactive
- advent-of-code
- geometry
- rotation-matrices
- integer-arithmetic
Identity, state and secrets: StatefulSets, volumes and ConfigMaps
The half of the Kubernetes object ladder that matters once you leave the tutorial: sticky pod identity, a PVC that survives a restart, and a base64 "Secret" that decodes to a joke.
- Interactive
- kubernetes
- devops
- flask
- statefulset
Ten manifests: Kubernetes from a single pod to a load-balanced service
A talk built from ten YAML files that climb the Kubernetes object ladder one rung at a time, and a simulated cluster you can apply, kill and scale yourself.
- Interactive
- kubernetes
- devops
- flask
- docker
Segmenting a point cloud with a 2004 image algorithm and a statistical distance
Felzenszwalb and Huttenlocher's 2004 graph segmentation, run over voxel Gaussians instead of pixels: a Hellinger-style distance between two Gaussians decides which edges are cheap enough to cross, and a synthetic room corner shows exactly where a plain position distance cannot tell a floor from a wall.
- Interactive
- segmentation
- point-cloud
- felzenszwalb-huttenlocher
- bhattacharyya-distance
A Gaussian that updates itself: streaming NDT statistics with Welford
A voxel that never stores a point, only a running mean and covariance, and the numerical argument for why it is computed the way it is: a naive variance formula that quietly goes negative, and an incremental one that does not.
- Interactive
- point-clouds
- welford
- numerical-stability
- gaussians
Eight children at a time: a sparse octree over a point cloud
A hand-rolled octree that never walks from the root and never allocates a node nobody visited: bit-peeling a voxel coordinate three bits at a time, hashed chunks instead of one giant tree, and a WebGL widget where dragging the depth slider is the whole argument.
- Interactive
- octree
- spatial-index
- point-cloud
- webgl
A convolution that never voxelizes
Every algorithm I had just benchmarked turned a point cloud into a voxel grid first. I spent three and a half months trying to build one that does not, wired into the same ResNet and U-Net Chris Choy designed for MinkowskiEngine so the comparison would be fair. No result was ever recorded.
- Interactive
- pytorch
- graph-neural-networks
- point-clouds
- geometric-deep-learning
When the batch doesn't fit: recursive splitting under CUDA OOM
A training loop that catches a CUDA out-of-memory error, halves whatever did not fit and tries again, and what going back to read it turned up: an except clause that catches far more than OOM, and a queue that quietly does not accumulate the gradient it looks like it should.
- Interactive
- pytorch
- cuda
- out-of-memory
- training
Scan a real object in your browser: the whole rig, start to finish
Print a board, point a projector and a webcam at a thing, and follow seven steps to a downloaded point cloud. Six posts of this series wired into one page, plus the two pieces that were missing: the local homography that turns a projector into a second camera, and guidance that tells you where to hold the board next.
- Interactive
- structured-light
- computer-vision
- camera-calibration
- point-cloud
A 2004 segmentation algorithm as a graph pooling layer
Felzenszwalb & Huttenlocher's merge rule, unchanged, driven by a learned edge metric instead of colour distance, so it coarsens a graph the way a neural network pools instead of the way a photograph gets segmented.
- Interactive
- graph-neural-networks
- pooling
- felzenszwalb-huttenlocher
- pytorch
From two rays to a point cloud: triangulation by the sine rule
Two rays, a known baseline, a triangle. The scanner derives depth from the law of sines instead of the usual least-squares midpoint. Then the messy real part: voxel downsampling, bounding-box cropping, and reading a mirror-image ghost handle back to its cause.
- Interactive
- structured-light
- computer-vision
- triangulation
- point-cloud
Deciding whether a pixel is lit: the part everyone gets wrong
Thresholding a Gray-code frame at 128 fails on shadows, glare, dark objects and the projector's own backlight. The fix separates direct from global light and lets the classifier answer "I don't know": the rule that makes the scanner work.
- Interactive
- structured-light
- computer-vision
- gray-code
- wasm
Segmentation fast enough for video
Felzenszwalb & Huttenlocher's 2004 graph segmentation is sorted edges plus union-find with a size-scaled threshold, reimplemented from the paper and run live on a webcam feed, with mean shift, normalised cuts and EM beside it on the same frame so the speed argument needs no prose.
- Interactive
- segmentation
- felzenszwalb-huttenlocher
- graph-algorithms
- union-find
Descriptors and the ratio test
Detecting a corner twice is the easy half. Recognising it again is the hard one: Lowe’s ratio test, a 5-pixel correctness threshold, and thirteen detector–descriptor pairs that mostly fail, rebuilt as a live oriented-FAST/rBRIEF matcher in Rust and WebAssembly.
- Interactive
- feature-descriptors
- ratio-test
- brief
- hamming-distance
Building a structured-light scanner out of a laptop and a phone
The hardware half of the scanner: why a projector and a camera have to sit where they sit, the crosshair trick that focuses both of them on the same point, and a browser rig (laptop as projector, phone as camera) that self-synchronises with no channel between the two devices at all.
- Interactive
- structured-light
- computer-vision
- gray-code
- camera
Repeatability: benchmarking 12 feature detectors on the Oxford affine dataset
Detect, warp by a known homography, re-detect, count the hits: the repeatability metric, the fairness trick that makes it meaningful, and a ranking that completely reshuffles between zoom, blur, viewpoint, lighting and JPEG.
- Interactive
- feature-detection
- corner-detection
- repeatability
- fast
Calibrating a camera from a pile of corners
Radial and tangential distortion, and a Levenberg–Marquardt bundle refinement over K, the distortion coefficients and every view's pose at once: the applied half of camera calibration, with a Canon 500D's real numbers at the end.
- Interactive
- computer-vision
- camera-calibration
- distortion
- bundle-adjustment
The frequency domain: aliasing, filtering before you downscale, and the convolution theorem
Halving an image can produce a pattern that was never in the scene. The fix is a circular mask on the DFT, and once you can paint on a spectrum, a lot of image processing stops being magic. With a Rust/WASM spectrum painter you can point at your own photos.
- Interactive
- fourier
- fft
- aliasing
- sampling
Finding a checkerboard, from adaptive threshold to sub-pixel saddle
The structured-light rig calibrates against a ChArUco board with one call to cv2.aruco. Here is what that call actually does, reimplemented independently in Rust: adaptive threshold, Suzuki–Abe contour tracing, a per-quad homography and a Hamming-corrected dictionary decode.
- Interactive
- computer-vision
- aruco
- charuco
- contour-tracing
Kernels: box, Gaussian, median, min/max, and deriving Sobel
Convolution is a weighted sum, and everything changes with the weights: from "average the neighbours" to "take the median instead" to "the weights are a derivative, so the output is an edge map", with a Rust/WASM bench you can point at your own camera.
- Interactive
- convolution
- filtering
- median-filter
- sobel
Look-up tables: log, gamma, contrast stretching, bit planes, equalisation
Five image-enhancement transforms turn out to be the same object, a 256-entry array, built five different ways. Histogram equalisation builds that array from the image itself, and CLAHE is where one array stops being enough. An interactive lab, with a live Rust/WASM CLAHE against the report’s own hidden test pattern.
- Interactive
- image-processing
- histogram-equalization
- clahe
- gamma-correction
Three segmentation algorithms, one image: a bake-off
Mean shift, normalised cuts and EM (with and without spatial coordinates) run on the same photograph in parallel Web Workers, so the failure taxonomy the report describes in prose becomes something you can point at.
- Interactive
- segmentation
- benchmarking
- comparison
- web-workers
Fitting Gaussians to pixels: EM, and what happens when you tell it where the pixels are
Model an image's colours as a mixture of K Gaussians and fit it by Expectation-Maximisation. Then append each pixel's (x, y) to its colour vector and watch the segments become compact, and the sky fall apart.
- Interactive
- segmentation
- expectation-maximisation
- gaussian-mixture-model
- clustering
Rebuilding SURF from the paper up
Box filters, the 0.912 correction, a 3×3 Hessian solve nobody skips willingly, and a reshape bug that quietly breaks a 64-D descriptor, implemented in Rust so the integral image my original code never got around to can finally make its case.
- Interactive
- surf
- feature-detection
- hessian
- integral-image
Zhang's method from first principles
Point a camera at a sheet of paper a few times from angles you never measure, and out falls the camera. A planar target collapses the projection to a homography, each homography constrains the image of the absolute conic, and a Cholesky hands back K in closed form.
- Interactive
- computer-vision
- camera-calibration
- homography
- svd
Finding every corner on a checkerboard
An X-corner is a saddle point of intensity, so one threshold on the Hessian eigenvalues finds every corner in the image, plus a few thousand impostors. The rest of the algorithm is four geometric filters that know what a checkerboard is.
- Interactive
- computer-vision
- camera-calibration
- corner-detection
- hessian
Painting a number onto the world with Gray code
You cannot search for where a projector pixel lands in a camera image: you label every column and row with a binary number and project the bits, one plane at a time. Why plain binary breaks at the boundaries, Gray code does not, and a widget that turns your screen into the projector.
- Interactive
- structured-light
- computer-vision
- gray-code
Normalised cuts: segmentation as an eigenvalue problem
Every pixel is a node, every edge weight says how alike two pixels are, and segmentation becomes graph partitioning. Minimum cut gets it wrong; Shi and Malik's fix turns the whole thing into an eigenvector you can look at.
- Interactive
- segmentation
- graph-partitioning
- eigenvectors
- lanczos
Warping pixels: affine transforms and the interpolation you forgot about
Every rotate() hides two decisions: where each output pixel comes from, and what value to give it when that lands between pixels. The five matrices, why you always run the map backwards, and a Warp Bench that shows nearest against bilinear under your pointer.
- Interactive
- image-processing
- affine-transforms
- interpolation
- rust
Which edge detector survives noise?
A Monte-Carlo benchmark of Canny, Sobel and the Laplacian on a synthetic step edge buried in Gaussian noise (30 000 trials, two curves, one surprise), rebuilt in Rust so it runs in your browser.
- Interactive
- edge-detection
- canny
- sobel
- laplacian
FERN: feature matching as a classification problem
Every keypoint becomes a class and matching becomes "which class is this?", answered by a few hundred one-bit brightness tests, plus a broadcasting slip that silently deletes the paper's random rotation, verified line by line and reproduced faithfully in Rust.
- Interactive
- stereo
- feature-matching
- naive-bayes
- rust
Mean shift, or how to segment an image without knowing how many regions there are
Pixels are a point cloud in Luv space; every region is a bump in its density. Comaniciu & Meer's three practical hacks (random search windows, a connected-component sanity check, and a radius-expansion pass) turn that idea into an algorithm.
- Interactive
- segmentation
- mean-shift
- clustering
- connected-components
Four algorithms, one benchmark: what actually won
Four segmentation algorithms, one harness, the same ScanNet scenes: reconstruction error bounds everything, voxel size beats architecture, and the numbers do not say which one to actually ship.
- Interactive
- python
- semantic-segmentation
- 3d-reconstruction
- evaluation-metrics
z = bf/d: epipolar geometry and the simplest triangulation there is
Depth from two photographs, derived from nothing but two similar triangles: what assumptions buy that simplicity, why the result is a shape and not a measurement, and a playground where you drag a disparity by hand and watch the uncertainty wedge breathe.
- Interactive
- stereo
- epipolar-geometry
- triangulation
- 3d-reconstruction
Vectorising a cost volume in NumPy (and why it still wasn't enough)
The gather trick that flattens a window into one index expression, the one-line broadcast it makes possible, the 26 GB that broadcast actually needs, and the loop the report retreated to instead, with the arithmetic shown.
- Interactive
- stereo
- numpy
- vectorisation
- memory
Grading a disparity map (and why 98% wrong is sometimes fine)
What bad-N actually measures, how a disparity scale can silently wreck a metric, and a live inspector where a headline number slides from catastrophic to fine purely by changing what you count.
- Interactive
- stereo
- disparity
- evaluation
- middlebury
Two images, one depth map
Dense stereo from first principles: why the search collapses to one scanline, what SAD, SSD and ZNCC actually cost, and a live disparity explorer in Rust and WebAssembly where hovering a pixel draws its cost curve.
- Interactive
- stereo
- disparity
- block-matching
- zncc
Reprojecting a 3D semantic map back into the camera
Comparing a labelled point cloud against image-space ground truth means pushing the images into 3D, matching them to the cloud, and scattering the labels back into the camera: the reprojection is exactly where a sparse map runs out of pixels to fill.
- Interactive
- python
- 3d-reconstruction
- semantic-segmentation
- reprojection
Matching instances when nothing tells you which is which
The assignment problem hiding inside every instance-segmentation metric: a greedy matcher with a proper conflict rule, an honest Hungarian-optimal alternative, and why the fresh-id trick for unmatched predictions matters. Rust in the browser, next to the pybind11 C++ it replaces.
- Interactive
- rust
- wasm
- instance-segmentation
- evaluation-metrics
Every number you can call 'accuracy' for a semantic map
Point accuracy, mean class accuracy, IoU, mIoU and FIoU disagree about the same prediction. Ported from a 2019 benchmark harness's metrics.py, with a widget that lets you win on one and lose on another.
- Interactive
- python
- semantic-segmentation
- evaluation-metrics
- computer-vision
The config file is the experiment
A declarative parser pattern from a 2019 3D-vision benchmark harness: fail loudly at parse time, quietly at run time, and let the output path double as the checkpoint.
- Interactive
- python
- config
- tooling
- research-infrastructure
APE, RPE, and why "how wrong is this trajectory" has five answers
A camera trajectory has two different kinds of wrongness, three ways to measure each, and an alignment step that can change the number reported by an order of magnitude: with a widget that lets the reader corrupt a path and watch it happen.
- Interactive
- slam
- trajectory
- odometry
- umeyama
Teaching a CUDA SLAM system to speak numpy
SWIG typemaps, buffer ownership and six lines of Eigen::Map that make a 2017 CUDA/Caffe SLAM codebase drivable frame-by-frame from Python: the seam a whole evaluation harness later ran through.
- Interactive
- swig
- python-bindings
- cuda
- cmake
Marking your own homework: EKF-SLAM vs RGB-D SLAM vs ORB-SLAM2
How three SLAM systems get scored against the same ground truth, the angle-wrapping line everyone forgets, and what the numbers say when a hand-rolled EKF is graded next to two published systems.
- Interactive
- slam
- ekf
- evaluation
- orb-slam2
EKF-SLAM: one covariance matrix for the robot and everything it has ever seen
The pose and every landmark in one state vector with one joint covariance, ported from a C++/Eigen RGB-D system to a room you can drive a robot around, with the covariance matrix drawn live, so the off-diagonal blocks that make SLAM work are finally visible.
- Interactive
- slam
- ekf
- kalman-filter
- state-estimation
How many particles, and how wrong can your first guess be?
Closing the Bayesian-filtering arc with the two questions every particle-filter engineer actually asks, answered by Monte-Carloing them live in the browser: Rust and WASM in a Web Worker, sweeping tens of millions of particle-updates behind a progress bar.
- Interactive
- particle-filter
- extended-kalman-filter
- monte-carlo
- rust
Four thousand guesses beat one Gaussian
A lighthouse keeper cannot tell a slow ship nearby from a fast one far away, and neither can a Kalman filter. So carry the whole posterior as a cloud of weighted samples instead. SIS, degeneracy, N_eff, resampling, jitter, and the number that makes the series worth reading.
- Interactive
- particle-filter
- bayesian-filtering
- state-estimation
- tracking
The Extended Kalman Filter, and the lie it tells
A first-order Taylor expansion, two Jacobians, and a filter that carries on as if the posterior were still Gaussian, shown on a lighthouse keeper tracking a ship from bearings alone, where it works beautifully and reports 46 m of cross-range confidence while sitting 680 m out along the beam.
- Interactive
- kalman-filter
- ekf
- state-estimation
- bayesian-filtering
The Kalman filter, derived by someone who had to implement it
The scalar case first: a noisy voltmeter, predict and correct in two lines each, and the gain that is nothing more than the optimal step size. Then the same filter in four states, tracking a ship you steer through fog.
- Interactive
- kalman-filter
- state-estimation
- bayesian-filtering
- tracking
Sixteen micro-benchmarks that made pure NumPy fast enough
Sixteen timeit scripts from the report's speed-tuning appendix, re-run on 2026 NumPy, plus a live JS benchmark table for the reader's own browser.
- Interactive
- neural-networks
- numpy
- performance
- benchmarking
I trained MNIST with iRPROP+ and lost to naive Bayes
The best optimiser in a 349-page study scored 70.19% on MNIST. A naive Bayes classifier I had written the month before scored 89.97%. Here is what I found when I went back to work out why.
- Interactive
- neural-networks
- mnist
- early-stopping
- rprop
De-noising an image with a Markov random field, and 22 ways to shape its neighbourhood
Write down what you believe about images as an energy function and de-noising becomes minimising it. The interesting part is replacing the textbook four-neighbour smoothness term with an arbitrary kernel window, and then testing 22 of them.
- Interactive
- markov-random-fields
- graphical-models
- icm
- image-denoising
Automatic Relevance Determination: letting the model delete your useless features
Give a Gaussian process four input dimensions, two of them pure noise bolted on for the exercise, and a separate length scale per dimension is enough for it to work out which two matter on its own.
- Interactive
- gaussian-process
- ard
- kernel-methods
- regression
Support vectors: why only a handful of your data points matter
The support vector machine sells itself on accuracy. Its real trick is sparsity: after training you can throw almost all your data away, and the Karush–Kuhn–Tucker conditions say exactly which points you have to keep.
- Interactive
- svm
- kernel-methods
- classification
- libsvm
A Gaussian process is just a prior over functions (and here are its knobs)
Marginalise the basis functions away and the prior lands on the function itself. Four hyperparameters, four visibly different kinds of curve, including two that are not curves at all.
- Interactive
- gaussian-process
- kernel-methods
- regression
- bayesian
How do you actually compare eleven optimisers?
XOR tells you about convergence speed and nothing about outliers. Six real Proben1 datasets, an error metric that means the same thing on all of them, and the PQα rule that decided when 3,960 training runs were each allowed to stop.
- Interactive
- neural-networks
- proben1
- early-stopping
- benchmarking
Training a neural network without gradients: genetic algorithms and PSO
Flatten every weight into one vector, evaluate sixty-four candidate networks in a single tensor contraction, and breed them. It loses badly to backprop, and why it loses is the whole lesson.
- Interactive
- neural-networks
- optimisation
- genetic-algorithms
- particle-swarm
QuickProp, ADAGRAD and Momentum: three ways to guess a learning rate
Three different bets about the error surface: fit a parabola and jump to its vertex, shrink each step by how far it has already moved, or just keep going. Two of them work. Then QuickProp blows up, because it does.
- Interactive
- neural-networks
- optimisation
- quickprop
- adagrad
The RPROP family: four ways to ignore the gradient's magnitude
RPROP keeps the sign of the gradient and throws the size away. Four variants, three lines of difference each, and visibly different trajectories.
- Interactive
- neural-networks
- optimisation
- rprop
- numpy
Backprop from first principles (no autograd, no frameworks)
Deriving backprop the whole way: one neuron, the sigmoid derivative, the output delta, the hidden-layer recursion, the matrix form, and the twelve lines of NumPy it collapses into.
- Interactive
- neural-networks
- backpropagation
- numpy
- from-scratch
Naive Bayes: the dumbest classifier that works
Assume sixty-four features are mutually independent, which they flatly are not, multiply their likelihoods and take the argmax. It reads handwriting at 89.97%, and two lines of the implementation are doing most of the work.
- Interactive
- naive-bayes
- mnist
- probability
- classification
Histogram of Oriented Gradients, from scratch
The one hand-designed feature that scores above 0.90 information gain on every MNIST digit: what it computes, why orientation beats position, and a live rose-overlay visualiser.
- Interactive
- computer-vision
- feature-engineering
- hog
- mnist
Teaching a computer to read by hand: feature engineering for OCR
Before deep learning ate OCR, you told the computer what to look at. Arc length, enclosed area, contour count: three small hypotheses about what makes a digit that digit, scored with Kononenko's information gain.
- Interactive
- ocr
- feature-engineering
- information-gain
- mnist
Error bars for free: Bayesian linear regression and the evidence
A prior on the weights buys three things least squares cannot give you: a band that widens where the data is missing, immunity to the order-9 catastrophe, and a number that ranks models on training data alone.
- Interactive
- regression
- bayesian
- model-selection
- evidence
Overfitting, explained by fitting a sine wave ten different ways
Ten noisy points, polynomials of order 0 to 9, and a training error that falls to zero while the curve becomes useless, plus the reason least squares is not a heuristic at all.
- Interactive
- regression
- overfitting
- maximum-likelihood
- numpy