byASB Ankiit Singh Book a scoping week
← Writing/Computer vision
TEARDOWNEV wireless charging · 2026

The model scored a hand 0.303 and a bolt 0.921

A foreign-object detector on an EV charging pad was failing in both directions at once. The team wanted to lower the threshold. The threshold was not the problem.

A wireless charging pad has one safety job before it energises: look at its own surface and decide whether anything is lying on it. A coin, a bolt, a bottle cap, a hand. Getting this wrong is asymmetric in an unpleasant way. A missed detection is a safety incident. A false alarm is a charging session that never starts, and enough of those and the product is returned.

This system was doing both. At night, headlight and torch reflections on the glossy pad fired constant alerts. In daylight, a hand covering roughly a third of the pad scored below threshold while a small dark bolt scored near the top of the range.

Frame 0417 · a human hand 0.303 Verdict — CLEAR ✗ (wrong)
Frame 1902 · a 12 mm bolt 0.921 Verdict — FOD ✓

The bolt's anomaly score was about three times the hand's. These scores are not calibrated probabilities: 0.921 does not mean a 92.1% chance of an object. The ordering exposed a problem with using this model score as the only route to detect the large hand.

Reading the hardware before reading the code

I was brought in remotely, and the first useful information arrived as screenshots of the running system. Screenshots carry far more than the picture inside them.

The frames had a distinctive magenta cast. That is the signature of a NoIR sensor — a camera with the infrared cut filter removed, so the red channel receives near-infrared it would normally reject. Combined with the resolution and framing, that identified the camera module. The window chrome around the image was OpenCV's own highgui decoration, which fixed the runtime. And the timestamps moving across successive frames implied a loop running at roughly 1.5 frames per second.

So before reviewing a line of code, the operating picture was: a Raspberry Pi with a Camera Module 3 NoIR (IMX708), a Python stack on OpenCV and Picamera2, inference through OpenVINO, and a control loop slow enough that any detection strategy depending on fast temporal sampling was already off the table.

Diagnosing from evidence first is not a party trick. It sets the constraints the fix has to live inside — in this case, a 1.5 FPS loop and a sensor that sees infrared.

Why the threshold was the wrong lever

The team's proposed fix was to lower the detection threshold so the hand would clear it. This is the most common response to a missed detection, and here it would have made the product worse.

A threshold is a single operating point on a curve. Moving it trades one error type for the other. This system was already producing false alarms at night — so lowering the threshold buys the hand at the cost of turning every headlight reflection into a refused charging session. You have not fixed anything; you have chosen which way to fail.

When a detector fails in both directions at once, the operating point is not the problem. The decision architecture is.

That reframing is the entire engagement. Everything after it is mechanical.

The six architectural causes

Six specific design decisions combined to produce that score inversion. Individually each is defensible. Together they guarantee the failure.

OR-based decision logicAny single indicator firing was enough to declare an object. With several noisy indicators, the false-positive rates add rather than intersect. Noise wins.
7–10 px minimum blobA fixed minimum size, applied per frame, with nothing requiring the blob to persist. Sensor noise and specular glints routinely exceed a 7 px threshold for one frame.
Auto-exposure lock at 1 sExposure locked one second after start, so the locked value depended on whatever the scene happened to be at that moment. Two boots under different light produced two different detectors.
No spatial persistenceNothing checked that a detection appeared in the same place across frames. A real object stays put; a reflection moves with the light source.
Per-frame heatmap normalisationMin-max rescaling per frame destroys absolute brightness between frames — a quiet frame is stretched until its noise looks exactly like a real anomaly.
CLAHE on the inputLocal contrast equalisation improves human-visible detail and removes the global brightness cue the detector actually needed. It was flattening the signal.

The normalisation problem, specifically

Of the six, per-frame normalisation is the one most worth internalising, because it appears constantly in vision code and looks harmless.

Rescaling each frame's heatmap to span 0–1 means the output no longer says "how anomalous is this frame" — it says "which part of this frame is most anomalous relative to the rest of this frame." An empty pad with faint sensor noise gets stretched until its loudest noise pixel reads 1.0. The comparison you needed — this frame against a known-clear reference — has been thrown away before the model sees it.

Why the model learned the wrong thing

The architecture explains the false alarms. It does not by itself explain why a hand scored 0.303. For that you have to look at what the model was taught was normal.

The pad is glossy. Throughout dataset collection it sat there mirroring whatever was above it — car undersides, ceiling lights, the general bright clutter of a workshop. Those reflections are large, soft-edged and bright, and they were present in a large share of the frames labelled as normal.

So the anomaly model learned, correctly and uselessly, that large soft bright shapes are the expected state of this surface.

A human hand over a charging pad is a large, soft-edged, relatively bright shape. It matches the learned background almost perfectly. A 12 mm bolt is small, hard-edged and dark — nothing in the training distribution looks like that, so it scores high.

The model was not broken. It answered the question it was actually asked. The question was wrong, and it was made wrong by the reflectivity of the surface being monitored.

This is the failure mode to watch for whenever you train an anomaly model on a reflective surface: your background class quietly absorbs the very features your dangerous objects share.

The fix: two tiers, different jobs

Retraining was possible but slow, and the constraint was to ship something deployable immediately. So the redesign kept the existing model and changed what it was responsible for.

Tier one — model-free, for large objects

A log-ratio comparison of the current frame against a stored reference frame of the clear pad. No model, no learned features, no opinion about what an object looks like. If a meaningful region of the pad differs substantially in brightness from the reference, something is on it.

This catches large objects — the hand — within two frames, regardless of what the anomaly model thinks. It is nearly impossible to fool with a large object, because a large object cannot fail to change the surface it sits on. And crucially, at 1.5 FPS, two frames is a little over a second.

Tier two — the anomaly model, narrowed

The model stays, but only for the case where it is genuinely strong: small foreign objects. And its output is now gated by AND-logic across four conditions rather than OR:

  • Size — the region must be large enough to matter
  • Peak — the anomaly response must be strong, not marginal
  • Not glare — the region must not match the signature of a specular reflection
  • Persistence — it must stay in the same place across frames

Requiring all four conditions makes the accepted set narrower than accepting any single indicator. The actual false-positive change depends on how the conditions interact. Their rates cannot simply be multiplied unless independence is established; size, brightness, glare and persistence may be correlated. The combined decision needs evaluation on representative frames.

Alongside this, exposure lock became deterministic rather than time-based, and normalisation moved to a fixed reference so absolute brightness survived to the decision.

The outcome

  • False-alarm detection efficiency reached 98%.
  • The large-object path no longer depends on the anomaly model's confidence. This addresses the identified scoring failure, while capture, reference quality and other miss mechanisms still require field validation.
  • Deployable the same night. No retraining cycle, no new dataset collection, no hardware change.

The last point is the commercially interesting one. The team had assumed the path forward was more data and another training run — weeks of work with an uncertain result. The actual fix was a restructure of how existing signals were combined.

How to interpret the reported 98% result

“False-alarm detection efficiency 98%” is the result reported for this engagement. The public record does not include the denominator, test-set composition or a complete confusion matrix. It should not be read as overall accuracy, 98% fewer false alarms, a miss rate or a safety certification. Those are different measures and cannot be reconstructed from the published figure.

For a new detector, define the evaluation before comparing a repair. Record which events count as true objects, which count as false alarms, and whether the unit is a frame, an object encounter or a charging session. A long sequence of frames from one reflection should not silently become thousands of independent test examples.

An evaluation checklist for computer vision false positives

  1. Separate failure types. Count false alarms and missed objects independently. Do not hide a consequential miss inside a high overall accuracy figure.
  2. Document the sample. Include the relevant object sizes, positions, lighting, reflections and camera conditions. Keep a separate set for evaluating changes rather than selecting only the frames used to tune them.
  3. Keep the comparison consistent. Run the old and new logic against the same captured input where possible, with the same event definitions and response deadline.
  4. Inspect correlated conditions. A glare filter and a brightness gate may fail together. Evaluate the joint decision instead of assuming their individual rates multiply.
  5. Measure latency and operational effect. Persistence can suppress noise but delays a decision. Check whether the action still occurs within the workflow's required time.
  6. Verify on hardware. Test exposure transitions, reference refresh, restart and sustained operation. Screenshot diagnosis identifies hypotheses; it does not replace field verification.

Google's classification metrics guide distinguishes precision and recall: how many positive predictions are correct, and how many actual positives are found. For this project, the published 98% label cannot be mapped to either without more evaluation data.

If your detector is doing this, check these first

Generalising out of the specifics, this is the order I would work through on any detector that is failing in both directions:

  1. Is it failing both ways at once? Inspect representative misses and false alarms before tuning. In this engagement, the evidence pointed to the surrounding architecture; that is not a universal diagnosis for every detector.
  2. Is your combination logic OR or AND? Review how the conditions combine and measure the resulting decision. Do not assume the signals are independent.
  3. Is anything normalised per frame? If so, you have discarded between-frame comparison. Normalise against a fixed reference instead.
  4. Does a detection have to persist? Real objects stay put. Noise and reflections do not. Persistence is the cheapest false-positive filter available and it costs one frame of latency.
  5. What does your background class actually contain? If the monitored surface is reflective, glossy or transparent, assume your normal class has absorbed features your target objects share.
  6. Can a model-free check cover the easy cases? Large, obvious, high-consequence detections should not depend on a learned model's confidence. Give them their own deterministic path.

Questions I get about this one

Why not just retrain on a better dataset?

You should, eventually — the dataset problem is real. But retraining does not fix OR-logic, per-frame normalisation or missing persistence, so you would spend weeks and still have a detector that fires on headlights. Fix the architecture first; then retrain against a background class that has been collected deliberately.

Doesn't a reference-frame comparison break when lighting changes?

It has to be managed — the reference needs refreshing when the scene legitimately changes, and the comparison is done in log-ratio space precisely because that is more stable against global illumination shifts than a raw difference. But an object on the pad changes the surface locally in a way a global lighting change does not.

Is 98% good enough for a safety system?

The public 98% label alone cannot establish safety. Its denominator and full evaluation set are not published. The repair removed dependence on model confidence for the large-object path, but the complete system still needs validation against its hazards and operating conditions.

How much of this needed access to the hardware?

None of the diagnosis. The camera, runtime and loop rate came from screenshots; the architectural causes came from reading the code against those constraints. Hardware access matters for verification, not for finding the fault.

If this sounds familiar

Send me the screenshot of it failing.

If you have a detector misbehaving in the field, send the evidence — frames, logs, scores. I'll tell you in one paragraph whether the problem is your threshold or your architecture, and whether a scoping week is worth it.

Start there →
Engagement details are published with the client unnamed. Every figure here — the 0.303 and 0.921 scores, the 7–10 px minimum blob, the 1 s exposure lock, the 98% result — comes from the real engagement. The generalised checklist is my own practice, not the client's.

Related: the project page for this engagement · all work