Skip to content
DRIFTWORK.STUDIO
← Projects
In progress

TankCam

Four photos a quarter-turn apart, unwrapped into one flat 360° surface, with a defect finder you train yourself by dragging boxes on it. Inspection DNA for equipment yards.

computer-visionopencvinspectionequipment

Visual surface inspection for a pressure cylinder. The operator takes four photos, a quarter-turn apart. The backend flattens them into a single continuous image of the whole outside, and then you teach it what corrosion looks like by dragging boxes on that image.

The camera lives in the wrong place

The app runs in a container. Docker on Windows goes through WSL2, and WSL2 does not pass a USB webcam into a Linux container — there is no /dev/video0 in there, and no flag that conjures one.

So the container never touches a camera. The browser captures instead: getUserMedia on the host, a hidden canvas grabbing frames at ~10fps, toBlob to JPEG, pushed up a WebSocket. The server decodes bytes, segments the tank, and sends overlay geometry back down the same socket. The browser draws the green silhouette on a <canvas> over the <video>.

A full round trip per frame, and it’s fine — 10fps of 80%-quality JPEG is nothing. The real cost is a constraint you have to know or you’ll lose an hour: getUserMedia demands a secure context, so it must be localhost. Hit the same server on a LAN IP over plain HTTP and the camera silently refuses.

The unwrap is the actual work

A point at angle θ on a cylinder lands at x = cx + R·sin(θ). So you don’t sample source columns linearly in x — you sample them along sin(θ). That inverts the projection and flattens the curve to constant scale.

Each shot unwraps to 120°, not 90. The extra 30° is overlap, so neighbours can be registered by cross-correlating one band’s left strip into the previous band’s right region. The weld seam and the stamped text give it something to lock onto. Below a 0.30 match score it stops trusting the correlation and falls back to pure geometry — which is what makes hand-rotation good enough. You have about ±30° of slop.

Deliberately stupid, behind the right seam

The defect model is a random forest over ~15 hand-built features — intensity moments, Laplacian variance, Sobel stats, a 10-bin LBP histogram. Sliding window, then greedy NMS.

That is not a shortcut, it’s the point: it trains in seconds on a CPU with no GPU, so the label → train → detect → accept-a-proposal loop stays tight enough that a human will actually do it. And it sits behind the same train() / detect() interface a PatchCore or a YOLO would later occupy. When there’s enough labelled data to deserve a real model, the seam is already cut.

It reports cross-validated accuracy next to train accuracy. Train 1.0 with poor CV isn’t a bug — that’s the model telling you it needs more examples.

Where it lies to you

Warts, because they’re the useful part:

  • The blur gate doesn’t exist. Focus is computed, displayed, and never checked. FOCUS_MIN is defined and referenced by nothing. Blurry shots sail through.
  • “Correct tank placed?” is a stub that always passes. It checks four views and an aspect ratio between 0.8 and 5.0 — but segmentation already rejects anything above 3.2, so the upper bound is unreachable. The UI renders it as a confident green PASS anyway. That’s the worst thing in here: a check that looks authoritative and tests nothing.
  • Valve detection never fails. If the circle finder comes back empty, it fabricates a centre and radius from geometry and returns them. valve_found is True whenever a silhouette exists. The o-ring “detector” is a crop of the top 30%.
  • The model doesn’t survive a restart. It’s written with joblib.dump and there is no joblib.load anywhere. The file sits in the volume while the app says “train a model first.”
  • Builds degrade silently. A shot whose segmentation failed is skipped — you can stitch two bands and get no warning.
  • Zero tests.

The segmentation comments are the best documentation in the repo, because they record the debugging. Otsu on a white tank on a cluttered bench grabs the entire frame; so it runs two cheap detectors and scores which one produced a plausible blob. The GrabCut prior is a fixed centre rectangle rather than the rough mask’s bbox — because the bbox prior kept swallowing the bright floor band and latching the body extent onto the floor.