
AVAILABLE FOR PROJECTS — WE REPLY WITHIN ONE WORKING DAY
WE BUILD COMPLEX THINGS.
SOFTWARE · AI · ROBOTICS · SYSTEMS
Software for machines that have to work outside the lab.
What we work on

Three things you can check.


A COCO metric that matches the reference exactly

A pose-graph back end on NumPy
Case study: stereo visual SLAM on KITTI
A robot with a camera and no externalpositioning has to work out its own motionfrom what it sees. Errors compound, andwrong data is silent.
01 — THE PROBLEM
02 — THE IDEA
Seven stages, and one of them lies.
- 40 px reprojection error → keyframes moved 2–4 m
03 — BUILD
13.3 % → 2.0 % → 1.26 %, AND WHY THE MIDDLE STEP WAS NOT A TUNING PROBLEM
$ python3 benchmarks/run.py --sequence data/kitti/2011_09_30/2011_09_30_drive_0027_sync sequence : data/kitti/2011_09_30/2011_09_30_drive_0027_sync frames : 0..1106 of 1106 baseline : 0.5372 m focal length : 707.09 px machine : 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, 8 logical cores vocabulary : training on 60 held-out frames (200..800 step 10) trained in 73.8 s running pipeline ... frame 400 keyframes 129 landmarks 10168 tracked 195 frame 800 keyframes 251 landmarks 15666 tracked 49 ==================================================================== frames processed : 1106 keyframes : 348 landmarks : 21393 mean tracked features : 163.6 loop closures accepted : 21 loop gate counters : {'queries': 347, 'appearance_candidates': 1555, 'rejected_consistency': 1062, 'rejected_few_matches': 467, 'rejected_geometry': 5, 'accepted': 21} translation error : 1.256 % rotation error : 0.01092 deg/m ATE RMSE : 1.276 m ground-truth path : 694.7 m estimated path : 696.6 m

RULED OUT
- The odometry itself. Raw frame-to-frame steps matched ground truth to a few centimetres the whole time, which is why the error was invisible in every per-frame diagnostic.
- Feature quality. A ratio test plus a mutual-best cross-check was already in place; the matcher was not the problem, trusting it was.
- A tuning problem in bundle adjustment. Instrumenting the windows showed them starting at 34–72 px reprojection RMSE and finishing at 32–60. The optimiser was not failing to converge, it was converging on a contradiction.
- The motion plausibility gate, which was added second and masked part of the symptom. With the association bug fixed it fires zero times on this drive, and the ablation table in the repository says so rather than quietly keeping the credit.
04 — THE MOMENT
benchmarks/output/trajectory.txt and ground_truth.txt.- ATE RMSE
- 3.331 m1.276 m
- TRANSLATION
- 2.020 %1.256 %

05 — RESULT
- 1.256 %
- 21 / 21
- 0.0e+00
REPRODUCE: git clone https://github.com/Pratyush150/stereo-visual-slam && python3 benchmarks/run.py --sequence data/kitti/2011_09_30/2011_09_30_drive_0027_sync
Numbers you can run yourself.
Every number on this page was produced by code in a public repository, on a public dataset, and the command that produces it is printed beside it.
REPRODUCE
git clone https://github.com/Pratyush150/stereo-visual-slam && cd stereo-visual-slam python3 tools/fetch_kitti.py --output data/kitti python3 benchmarks/run.py --sequence data/kitti/2011_09_30/2011_09_30_drive_0027_sync
| translation (%) | rotation (deg/m) | ATE RMSE (m) | |
|---|---|---|---|
| before loop closure | 2.020 | 0.01125 | 3.331 |
| after loop closure | 1.256 | 0.01092 | 1.276 |
The first full-sequence run scored 13.3%. Every descriptor match between consecutive keyframes was being turned into a landmark observation and roughly half of them were wrong; the per-frame poses were fine throughout. Gating the association on a reprojection check took odometry from 13.3% to 2.0%. That is in the repository, with the instrumented bundle-adjustment residuals that found it.
REPRODUCE
git clone https://github.com/Pratyush150/object-detection-benchmark && cd object-detection-benchmark python3 tools/fetch_assets.py --dest assets python3 benchmarks/verify_metric.py --annotations assets/annotations/instances_val2017.json --detections assets/cache/fp32_<hash>.json
| metric | detbench | pycocotools | abs diff |
|---|---|---|---|
| AP @ 0.50:0.95, all | 0.366292468048 | 0.366292468048 | 0.0e+00 |
| AP @ 0.50, all | 0.514783906518 | 0.514783906518 | 0.0e+00 |
| AP @ 0.75, all | 0.398023047115 | 0.398023047115 | 0.0e+00 |
| AP @ 0.50:0.95, small | 0.173954129172 | 0.173954129172 | 0.0e+00 |
| AP @ 0.50:0.95, medium | 0.404532620277 | 0.404532620277 | 0.0e+00 |
| AP @ 0.50:0.95, large | 0.520095107021 | 0.520095107021 | 0.0e+00 |
| AR @ 0.50:0.95, maxDets=1 | 0.303619498914 | 0.303619498914 | 0.0e+00 |
| AR @ 0.50:0.95, maxDets=10 | 0.493089837060 | 0.493089837060 | 0.0e+00 |
| AR @ 0.50:0.95, maxDets=100 | 0.531297782877 | 0.531297782877 | 0.0e+00 |
| AR @ 0.50:0.95, small | 0.289230886977 | 0.289230886977 | 0.0e+00 |
| AR @ 0.50:0.95, medium | 0.592759654029 | 0.592759654029 | 0.0e+00 |
| AR @ 0.50:0.95, large | 0.706709325495 | 0.706709325495 | 0.0e+00 |
pycocotools is not a dependency of the package. It appears only in the test suite and in benchmarks/verify_metric.py, to hold the from-scratch implementation honest.
REPRODUCE
python3 benchmarks/run_sweep.py --assets assets --annotations assets/annotations/instances_val2017.json --images assets/val2017 python3 benchmarks/run_latency.py --assets assets --sweep benchmarks/results/sweep.json
| variant | size | mAP | Δ mAP | inference p50 | speed-up |
|---|---|---|---|---|---|
| fp32 | 12.82 MB | 36.63 | — | 82.4 ms | 1.00× |
| int8-static-c16 | 3.66 MB | 35.70 | −0.93 | 34.9 ms | 2.36× |
| int8-static | 3.66 MB | 35.22 | −1.41 | 35.8 ms | 2.30× |
| int8-static-pertensor | 3.58 MB | 34.24 | −2.39 | 34.2 ms | 2.41× |
| int8-static-notail | 3.60 MB | 0.00 | −36.63 | 28.3 ms | 2.91× |
The unflattering number: Ultralytics publishes 37.3 mAP for YOLOv8n on val2017. This measures 36.63 on 4,872 images and 36.57 on all 5,000 with the same settings — about 0.7 points short. It is not a disagreement about the metric, which is verified above to twelve decimal places on these exact detections. The five causes are (1) single-label decode rather than multi-label NMS, (2) NMS at IoU 0.7 with 300 boxes, tuned per release upstream, (3) letterbox padding colour, upscaling and offset rounding, which move boxes by about a pixel where AP75 lives, (4) export settings — opset 13, static 640×640, no graph simplification, NMS outside the graph, and (5) 4,872 images rather than 5,000, because 128 are reserved for quantisation calibration; that split is worth 0.06 points. Every one of them is a command-line flag in the repository.
REPRODUCE
git clone https://github.com/Pratyush150/pose-graph-slam && cd pose-graph-slam python3 tools/fetch_datasets.py intel manhattan manhattan_gt sphere2500 sphere2500_gt python3 benchmarks/run_benchmarks.py python3 tools/gtsam_crosscheck.py # the outside column
| dataset | our final chi2 | GTSAM final chi2 | agreement |
|---|---|---|---|
| CSAIL | 40.55 | 40.56 | same to 3+ figures |
| intel | 45 | 45.01 | same to 3+ figures |
| parking-garage | 1.268 | 1.268 | same to 3+ figures |
| manhattan | 3549 | 3549 | same to 3+ figures |
| sphere2500 | 1351 | 1351 | same to 3+ figures |
| cubicle | 2746 | 2749 | within 0.1% |
| torus3D | 5.99e+04 | 5.996e+04 | within 0.1% |
| tinyGrid3D | 18.63 | 18.66 | within 0.2% |
| smallGrid3D | 1036 | 1039 | within 0.3% |
| sphere_bignoise | 7.370e+06 | 7.814e+06 | ours lower by 6% |
| city10000 | 512 | 1.836e+07 | ours lower by 4 orders |
| MIT | 770.2 | 4.414e+09 | GTSAM took no step |
Reading the last three rows honestly: on sphere_bignoise, city10000 and MIT the two solvers did not land in the same place, and ours landed lower. That is not a claim to have beaten GTSAM — it is an untuned comparison in both directions, and torus3D and sphere_bignoise are local minima on our side too. The repository says so in the same table.
3,449 TESTS COLLECTED · 16 REPOSITORIES · A CI WORKFLOW ON EVERY ONE READ ANY OF IT
Five layers, and the way each one fails.
BOARD BOUNDARY
Someone else's board, and what our code has to survive on it
Pixhawk-class autopilots, Jetson and Raspberry Pi companions, serial and USB links, encoders, Modbus devices — your hardware, not ours. We write the firmware and the drivers that run on it and the software that talks to it. Everything below this line has a physical failure mode that has to be handled in code.
FIRMWARE & TRANSPORT
The link, and everything that lies about it
MAVLink over serial, UDP and TCP; rate control, offboard setpoints and link diagnostics. A heartbeat tells you the link is up. It does not tell you the stream is alive.
MIDDLEWARE
ROS 2, and the acquisition layer for machines that are not robots
Bringup, lifecycle, geodesy, missions, geofence and state machines on the robot side; Modbus and OPC-UA acquisition with alarms and a historian on the plant side.
PERCEPTION & ESTIMATION
Where wrong data is silent
Stereo and LiDAR SLAM, pose-graph back ends, PnP and Gauss-Newton on SE(3), Kalman and extended Kalman filters, detection and tracking on the edge.
INTERFACE
The part the operator actually touches
Fleet dashboards, ground stations, mobile telemetry apps. Built against the same message definitions as the robot, so a schema change breaks the build rather than the shift.
LIMITS
- What runs offline: every benchmark and every test on this page. Heavy dependencies are guarded, so the suites pass with pymavlink, ONNX Runtime and the datasets absent.
- What needs a network: dataset fetches, the LLM-backed assistants, and the ONNX export step. Nothing else.
- The SLAM and detection work is evaluated on public benchmarks — KITTI and COCO — not on your sensor rig. Numbers on your data will differ, and the first thing we would do is measure them.
- Several repositories are reference implementations rather than production systems: they exist to be read and to be correct, not to be deployed unmodified.
- We write software, not hardware. Firmware, drivers, embedded C++ and board bring-up in software terms for boards you already have: yes. Building, wiring, assembling or repairing a machine, PCB and mechanical design, or anything that would have to be shipped to us: no.
- We decline work we would do badly. That currently includes web3, ad tech, growth automation, and anything where the brief is a design comp and a deadline.
Things we wanted to know.
Can a robot work out where it is from two cameras and nothing else?
STATUS: SHIPPED
Can a pose graph disagree with itself?

STATUS: SHIPPED
What does INT8 actually cost?
STATUS: SHIPPED
Can a swarm avoid itself when one agent is late?
STATUS: SHIPPED
Can a bag-of-words vocabulary be small and still find a revisit?
STATUS: ABANDONED
Can detections be scored without trusting the scorer?

STATUS: SHIPPED
How an engagement runs.
- 01IDEA
- 02DISCOVERYTell us what is going wrong
- 03PROTOTYPEPaid diagnosis or scoping first
- 04ENGINEERINGImplementation with visible progress
- 05TESTING
- 06DEPLOYMENT
- 07MAINTENANCEHandover you can maintain
One team. One conversation. End-to-end execution.
We say no to work we would do badly.

You will be talking to the person writing the code.
There is no account manager between you and the work. The person who reads your logs is the person who writes the fix, which is why the first thing you get back is a diagnosis rather than a quote.
The work runs across four layers — flight stacks and control, simulation and testing, industrial acquisition and AI, and the interfaces people actually operate. They are the same job at different altitudes, and the seams between them are where projects usually break.
A wrong yes costs you more than an honest no.
- Every engagement opens with a written root-cause report. It stands on its own — you can act on it whether or not the implementation is bought here.
- Results on this page were produced by public code on public data, and the command that produces each one is printed beside it. Including the one that is worse than the published figure.
- We write the software, including the software that runs on your hardware — firmware for your flight controller, drivers, embedded C++, ROS 2 nodes, MAVLink integration, perception. We do not build, wire, assemble or repair the machine, and nothing has to be shipped to us: logs, parameter dumps and a photo of the setup are what we work from.
- Remote robotics debugging has unbounded scope, so it is sold as diagnosis first and quoted afterwards. Work that would be done badly is declined rather than accepted.
Bring us a problem.
Describe the machine or the process, the versions, and send the evidence. We reply within one working day with an honest answer about whether we can help and what it would cost. If it is not something we can do well, we will say so.
Every engagement starts with a written root-cause report, at a fixed low price. You get an explanation you can act on even if you never hire us for the fix.