Guilhem Carmouze

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.

Repository: erpkit

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.

Repository: microsplat

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.

Repository: gaussian-projection-bench

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

splat-navmap

A study of when an occupancy grid sliced from a 3D Gaussian Splatting scene makes an A* planner drive through walls or refuse a doorway, on synthetic flats whose true geometry is known.

Repository: splat-navmap

One result

28.8→1.6%

Paths that enter real geometry at an opacity threshold of 0.5, with moderate defects: centre counting, then footprint accumulation (1,000 start-goal pairs on 10 synthetic flats). The map with the lower IoU plans better (0.652 against 0.663). Centre counting is best at 0.3, where 1.8% collide but 4.9% of pairs become unreachable; the plateau above 0.5 follows from the defect magnitudes I chose.

What the clip shows

The opacity threshold swept from 0.05 to 0.95 on one flat (seed 4) and one start-goal pair chosen by hand. For centre counting and for footprint accumulation: the Gaussians seen from above, the extracted grid with its phantom and missing cells, and the A* path, with red crosses where it enters real geometry. The Gaussians are synthetic surfels with modelled defects, not trained splats.

Where it comes from

My internship report derived an occupancy grid from a slice of a supplied 3DGS scene and planned on it with A*, and noted that how Gaussian opacity relates to collision geometry was only partly validated. This project studies it on synthetic flats whose geometry is known. Nothing from the internship is reused.

Stack

  • Python
  • NumPy
  • SciPy
  • 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.

Repository: cone-ekf-slam

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.

Repository: amr-traffic-lab

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.

Repository: usine40-cell-pipeline

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?

Repository: visual-quality-gate

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