Lab
Side projects
Small studies that take one question left open by the real work and answer it with a measurement.
These are personal side projects, built in October 2026 with AI assistance: their commits carry a Co-Authored-By trailer. Every number below is reproduced by a script committed in the repository.
erpkit
A tested NumPy toolkit for 360-degree image geometry (equirectangular, cubemap, pinhole, multi-view rigs) that measures what stitching views into a panorama really costs.
One result
5.6→1.06
Seam step ratio under a ±5% exposure mismatch between views, strict cube against 96° faces with feathering (mean over 3 seeds, 1 = invisible). Feathering hides the mismatch rather than fixing it: WS-PSNR stays at 35.5 dB.
What the clip shows
A 90° pinhole camera sweeps over an equirectangular panorama of a procedural room, its footprint drawn on the panorama next to the extracted view. Below, a 14-view rig is assembled view by view on a map of how many views see each direction.
Where it comes from
The internship stitched six pinhole views into panoramas with an overlapped cubemap. Face size, overlap and feather width are settings whose cost this project quantifies, with its own code and generated images.
Stack
- Python
- NumPy
- Pillow
- pytest
microsplat
3D Gaussian Splatting small enough to read in one sitting: a NumPy reference rasteriser, a differentiable PyTorch twin, and tests that pin every equation.
One result
33.6dB
PSNR on held-out views of the ray-traced shapes scene after 3,000 iterations from random Gaussians, with adaptive density control (mean of 3 seeds).
What the clip shows
Four panels through an optimisation that starts from random Gaussians, then an orbit of held-out views: the ray-traced target, the render, the absolute error and the outlines of the projected Gaussians.
Where it comes from
During the internship everything went through existing renderers: 3DGRUT, Splatfacto and DISCOVERSE. This project opens the renderer: the forward pass rewritten from the papers, made differentiable, one test per equation.
Stack
- Python
- NumPy
- PyTorch
- pytest
gaussian-projection-bench
A numerical study of the two ways a 3D Gaussian is turned into a 2D one, EWA linearisation and the unscented transform, through pinhole, fisheye and equirectangular cameras, against a Monte-Carlo reference whose own noise floor is reported.
One result
18→44px
Splat standard deviation at which the projection error passes half a pixel (2-Wasserstein) at 45° off-axis in a pinhole camera: EWA with the exact Jacobian, then the unscented transform with 3DGUT’s sigma points.
What the clip shows
An isotropic 3D Gaussian slides to the edge of a 180° fisheye image, then to the pole of an equirectangular image. Grey: the Monte-Carlo density of its projection. Orange: the EWA ellipse. Blue: the unscented-transform ellipse and its seven sigma points.
Where it comes from
Most Gaussian renderers assume a perspective camera, which is why the internship rendered six pinhole views and stitched them. This project measures what each approximation costs, in pixels, including near the poles of a panorama.
Stack
- Python
- NumPy
- Matplotlib
- pytest
cone-ekf-slam
EKF localisation and EKF-SLAM on simulated Formula Student cone tracks seen through a limited field of view, with the consistency tests (NEES, NIS) that show when the filter’s own uncertainty can no longer be trusted.
One result
17 / 50→1 / 50
runs in which nearest-neighbour association matched a wrong cone, with a sensor range of 4 m (that of the original 2D simulator, 1.1 cones per scan) then 15 m (6.4 cones per scan). 5 tracks, 10 noise seeds each.
What the clip shows
A closed cone track from above: the true path in grey, the EKF-SLAM estimate in red, the sensor sector and the 99% ellipses of the pose and of every mapped cone. The pose uncertainty grows to 0.65 m (1-sigma) before the first cones are seen again at t = 37.5 s, then drops to 0.03 m, and the ellipses of the cones out of view shrink with it (track seed 7, 1.3 laps). Two strip charts below follow the pose NEES and the uncertainties.
Where it comes from
In the TLSe Racing driverless team my part is the simulator and the field-of-view sensor model that picks the cones the car can see; the planners are my teammates’ work. This project studies the stage between the two: estimating where the car and the cones are from noisy detections. Simulation only: it has never run on a car and shares no code or track with the team’s repositories.
Stack
- Python
- NumPy
- SciPy
- Matplotlib
- pytest
amr-traffic-lab
A seeded simulation study of Industry 4.0 smart-factory (Usine 4.0) intralogistics: how many autonomous mobile robots a factory aisle can take before it jams, with the no-collision invariant checked on every tick.
One result
0 / 600
one-hour runs gridlocked with the reservation manager, over three layouts. On the open floor it also delivers 24% more orders per hour than the naive manager with 16 robots (528.9 against 427.2).
What the clip shows
Two replays of the same factory floor, two halls joined by a single-lane corridor, with the same 12 robots. Left: with the naive manager the robots meet head-on in the corridor and stop for good. Right: with the reservation manager they take turns and the delivered-orders counter keeps rising.
Where it comes from
The internship planner moved one robot on an empty map. This project adds time, a reservation table and Conflict-Based Search, and measures the fleet. It is independent of the final-year class project on Usine 4.0.
Stack
- Python
- pytest
- Matplotlib
usine40-cell-pipeline
A simulated Industry 4.0 (Usine 4.0) production cell sent through OPC UA, MQTT, PostgreSQL and Grafana, with the OEE (overall equipment effectiveness) on the dashboard checked against the simulator’s own event log, and every latency and loss measured.
One result
0.000pp
Largest gap between the OEE stored by the pipeline and the OEE recomputed from the simulator’s event log, over 3,600 station-windows of 30 s. The only way I found to break it is to lose samples: a 60 s broker outage at QoS 0 leaves 45 of 240 windows with a wrong OEE.
What the clip shows
The live Grafana dashboard during a scripted scenario: nominal production, an injected breakdown that fires the fault alert, a gateway kill that turns the timeline to NO DATA and fires the stale-data alert, then the recovery. The frames are real screenshots of the provisioned dashboard; only the caption strip is added.
Where it comes from
At AIST I prepared a ROS 2 interface with stale-frame rejection, velocity clamps and a dead-man timer. Here I wanted the same discipline on the machine side of a factory: stamp every value at the source, never trust a message because it arrived, count what is lost. It is independent of the final-year class project on Usine 4.0.
Stack
- Python
- OPC UA
- MQTT
- PostgreSQL
- Grafana
- Docker
- pytest
Images: MVTec AD (Bergmann et al., CVPR 2019), CC BY-NC-SA 4.0, adapted (heat maps and captions added).
visual-quality-gate
A training-free visual quality gate for an Industry 4.0 (Usine 4.0) line: PaDiM and PatchCore re-implemented in PyTorch, measured on five MVTec AD categories and judged on a line manager’s question: how many good parts do I refuse to stop how many defects?
One result
10.5%
of good parts refused by PatchCore WR50-10% when its threshold, set from held-out good parts, aims at 5%: 43 of 408 over 3 seeds, 2.1 times the target, while 6.3% of defective parts (85 of 1,353) still get through.
What the clip shows
Five test parts scored by PatchCore WR50-10% (seed 0), each with its anomaly heat map, score, threshold and OK or NOK verdict: the median caught defect, the median accepted good part, the caught defect closest to the threshold, the worst escape (a defective part that passed) and the worst false reject (a good part refused).
Where it comes from
In my first year of the engineering cycle I co-wrote with a classmate a colour-ball detector in plain C, which works when the thing to find is a colour you can name in advance. This project is its learned-feature successor: the detector only sees good parts, and the threshold is set from good parts too. It is independent of the Usine 4.0 class project.
Stack
- Python
- PyTorch
- NumPy
- pytest