Skip to main content

Jason Slade

IIoT Director | SCADA | MQTT | Controls Engineering

Teaching a Camera to Read a 60-Year-Old Gas Meter

Field Notes from a Home Lab

My gas meter is a four-dial analog unit bolted to the outside wall of the house. For years the entire reading pipeline was me: once a month, stand in the weather, squint at four needles, type a number into a spreadsheet. One sample every thirty days. If the boiler developed a slow leak or the furnace started burning more than it should, I’d find out when the bill landed — late and heavier than it needed to be.

I wanted hourly readings. But the utility owns the meter and I can’t swap it for a digital one even if I wanted to. And honestly, the mechanical dials are fine — they’ve been turning for decades and they’ll probably outlast everything else on the property. The problem was never the hardware. It was the interface. So I built a computer vision pipeline that reads the dials from a photograph and publishes the number to MQTT. No meter replacement, no electrician, no permission slip. A camera, Python, and OpenCV.

The meter

Four dials, left to right: one million, one hundred thousand, ten thousand, one thousand. Each is a needle pointing at a digit. Here’s the first thing that trips people up: the dials don’t all turn the same way. Mid-century meter designers had opinions — the directions alternate CCW, CW, CCW, CW. Assume they all turn clockwise and the fourth dial reports nonsense every time. I made that mistake on my first pass and spent an afternoon staring at a reading that was off until I walked outside and watched the last needle move backwards.

The meter is outdoors, so every shot contends with variable lighting, shadows, and the occasional spider web stretched across the face. Nothing a basement setup would need to think about.

Calibration

The first step is telling the pipeline where each dial lives in the frame. I record four centers on a reference image and save them to calibration.json. For my meter that’s the 1M dial at (388, 428), the 100K at (472, 435), the 10K at (550, 438), and the 1K at (635, 442). Four points. That’s the entire calibration surface. The pipeline loads them at startup, crops a region around each dial, and runs headless from there. No deep learning, no bounding-box models, no annotation tools. Calibration is the part you do exactly once, so I made it trivial enough that it actually gets done.

{
  "dials": [
    { "id": 0, "label": "1,000,000", "x": 388, "y": 428, "direction": "ccw" },
    { "id": 1, "label": "100,000",   "x": 472, "y": 435, "direction": "cw"  },
    { "id": 2, "label": "10,000",    "x": 550, "y": 438, "direction": "ccw" },
    { "id": 3, "label": "1,000",     "x": 635, "y": 442, "direction": "cw"  }
  ]
}

Reading the needles — the hard part

This is where the pipeline earns its keep. Each dial region is a needle pointing somewhere on the 0–9 scale, but finding it in a photograph of an outdoor meter is noisy: uneven lighting, reflections off the glass, a thin needle sometimes half in shadow.

The pipeline attacks each region in four steps. First, grayscale plus CLAHE contrast enhancement to pull the needle out of flat or washed-out lighting where a simple threshold would lose it. Second, radial-edge filtering: compute the gradient at each pixel, then keep only gradients that run perpendicular to the radial direction from the dial center. A needle pointing outward produces edges tangent to the circumference; gradients running along the needle’s length are almost certainly shadows or the dial’s printed markings. Filtering by direction isolates the needle from the tick marks and the rim. Third, HoughLines finds the dominant line through the dial center — that’s the needle’s axis.

But a line has no direction. A needle at 3 and a needle at 8 are the same line, 180 degrees apart. To break the ambiguity I compute the center of mass of the bright pixels — the needle body — and pick the direction that points toward the heavier side. From there it’s arithmetic:

# clockwise dial
digit = floor(angle / 36)

# counter-clockwise dial
digit = floor((360 - angle) / 36)

Four dials, four digits, and the meter speaks.

This is deliberately old-school. No neural net, no segmentation model, no GPU. Just geometry and edge math. For four dials, you don’t need anything heavier.

Aligning for a drone

The dial coordinates are fixed at calibration time, so if the next photo is taken from a different angle or distance, every region is off. And I can’t count on a perfectly framed shot — the endgame is a drone that won’t hover in exactly the same spot every time.

The fix is ORB feature matching. The pipeline extracts keypoints from the reference and from each new frame, matches them, fits a homography with RANSAC, and warps the new frame so the dials land exactly where calibration expects. Same idea behind panorama stitching and AR. ORB is fast enough to run on a Raspberry Pi. It won’t fix a complete mismatch — the drone can’t point at the sky and expect a reading — but it handles the kind of drift you get from real-world repositioning.

Wiring it up

Once the pipeline has a reading, the rest is plumbing. It publishes JSON to the MQTT broker at 192.168.0.73 on topic home/gas_meter/reading, retained, QoS 1:

{
  "value": 8231,
  "unit": "cubic_feet",
  "timestamp": "2026-08-13T14:00:00Z"
}

Home Assistant picks it up as a sensor. It also lands in Ignition SCADA alongside everything else on the network. From there: graphs, alerts, a number I can query from anywhere.

The reference image meter.jpg reads 8231 cubic feet — ground truth, verified the old way by walking outside and reading the dials. The pipeline also reports each dial’s angle and a confidence value, so when something looks off I can see which dial got confused instead of guessing.

Failing honestly

Not every frame yields a reading. Bad alignment, low light, an obstruction — the pipeline returns nothing instead of guessing. Because MQTT retains the last good value, Home Assistant holds the last reading until the next good capture.

This is the hardest habit to build with sensor systems and also the most important: a null is better than a wrong number. A wrong number looks like real data. It feeds graphs and triggers automations and gets quoted back at you later. I’d rather the system say “I don’t know” than confidently report the wrong thing.

What this unlocks

Hourly gas data means I can plot consumption against outdoor temperature and actually measure boiler efficiency. I can catch a blown pilot within an hour instead of a month. I can separate heating-season usage from baseline. None of that was possible with a monthly manual read and a spreadsheet.

The meter didn’t change. It’s the same mechanical dials that have been spinning for decades. I just gave them an API. That’s the pattern I keep coming back to: computer vision on legacy analog infrastructure beats replacing every device with a smart one. Don’t rip things out. Point a camera at them and make them legible to software.

To be continued

The CV pipeline is done and verified. It calibrates, aligns, reads the needles, publishes to MQTT, and fails cleanly when the image isn’t good enough. The next phase moves the camera off the tripod and into the air: I’m adding a DJI Mini 4 Pro running the DJI Mobile SDK v5. The drone flies a waypoint mission to a fixed position facing the meter, captures the shot, and hands it off to the same pipeline — read, publish, done. A drone reading an analog meter beats a fixed camera that needs weatherization and power, and it definitely beats walking outside with a flashlight.

I’ll pick this up once the drone components and the waypoint app are in place. The CV side is solid. Now it needs wings.


Jason Slade is an IIoT Director at Horizon Controls and an automation consultant for FDA-regulated manufacturing. He writes about the intersection of industrial controls and hands-on experimentation.

Follow-up: it turned out the meter was already broadcasting — reading the same meter off the air with an SDR made the camera rig unnecessary.

← All posts

→ Subscribe by RSS