Inspiration
Crack width is what separates a cosmetic crack from one an engineer needs to see. Today an inspector holds a comparator card against the wall, reads a width by eye and writes it down. That reading is not traceable, not repeatable and carries no uncertainty. ASCE's 2025 Infrastructure Report Card counts 623,218 bridges in the US, and about a third of them (221,791 spans) need repair work or replacement. We wanted a width you could still defend six months later, and a tool that says so when it can't give you one.
What it does
Tape a printed marker of known size flat on the wall next to the crack, then walk past with a phone. Hairline:
- scores every frame for focus and exposure and finds the marker;
- recovers the wall plane from the marker, so the scale in pixels per millimetre is correct across the frame even when the camera is not square on;
- finds dark, thin, long components that are really darker than the surface around them, and cuts crack networks at their junctions so each run is measured on its own;
- reads intensity profiles across each crack, perpendicular to it on the wall rather than on the screen;
- corrects each width for lens blur, measured from the marker's own printed edges in the same frame;
- reports a width distribution per crack with an uncertainty budget, as a schedule you can export to CSV.
It refuses, by name: no marker, marker too small, surface too oblique, out of focus, exposure clipped, crack finer than the photo can resolve, too faint to be a crack. Each gets a code, a reason and what to do next. A crack below the resolution limit gets an upper bound instead of a width.
Real photos without our marker. In manual scale mode you pick two points on a ruler, crack card or crack monitor in the photo and type in the distance between them. The uncertainty is charged for what two points can't tell you (see How we built it). A black-hat segmentation mode handles rough real surfaces where the default threshold breaks a crack into fragments.
Accuracy, on synthetic targets of known width (stand-off, angle, defocus, lighting and noise varied; 266 readings attempted):
| Measured | 103 |
| Declined, each with a named reason | 163 (61%) |
| Median error | −0.61%, which is 6 micrometres |
| Absolute error, 95th percentile | 2.1% (0.036 mm) |
| Worst single error | 5.6% (0.056 mm) |
| Stated 95% interval contained the truth | 98.1% |
An uncertainty that doesn't cover the error is decoration.
Real photos with a manual scale. The black-hat settings were tuned on 16 real photos with no scale, detection only. It found 12 of 18 hand-traced cracks, up from 1, at a cost of 14.4 false components per image, mostly panel joints on two clad walls. The settings were then frozen, and two held-out photos were run once:
| Test photo | Found | Measured, p95 ± U (k = 2) | Refused |
|---|---|---|---|
| DSC07068, Avongard card, 80 mm span, ±2.21% scale | 6 | 0.485 ± 0.060, 0.711 ± 0.079, 0.519 ± 0.080 mm (medians 0.386, 0.493, 0.462) | 3, BELOW_RESOLUTION, under 0.357 mm |
| Dnipro crack monitor, 40 mm span, ±3.50% scale | 0 | none | none |
Neither photo has a true width, and the card's comparator lines don't sit on the crack, so this is not an accuracy claim. Read by eye against the card, the crack looks like a hairline of roughly 0.2 to 0.3 mm. The measured widths sit on the high side of that, and all three runs are within 1.5 px of the resolution floor. The Dnipro photo is a failure. The likely cause is the length filter (20 widths at the 4 mm expected width is about 1,020 px); this is unconfirmed, and the result is reported as it ran. In the web app, where leave-out areas are rectangles rather than the test run's polygons, the same photo gives three stretches with medians of 0.35 to 0.49 mm and no refusals.
How we built it
OpenCV 5.0.0, pinned to opencv-python-headless==5.0.0.93. Unpinned, pip resolves to 4.14.x, which shipped after 5.0.0, so import
visioncore raises on a 4.x wheel.
Scale. cv2.aruco.ArucoDetector with sub-pixel corner refinement finds the marker (it is in the main wheel in OpenCV 5), and
cv2.getPerspectiveTransform gives the homography. At each sample we use the 2×2 Jacobian of the plane map: the crack's normal on the wall
maps to an image direction, and one pixel along that direction is exactly 1/|J n| mm. Across the sweep, scale recovery had a median error
of 0.082% and a worst case of 0.284%.
Segmentation. cv2.adaptiveThreshold (Gaussian), morphologyEx, connectedComponentsWithStats and findContours. The threshold
constant is measured per image from the surface's own texture spread. For rough real surfaces there is a MORPH_BLACKHAT mode with
hysteresis.
Width. A crack of width w blurred by σ has a half-depth width that levels off at 2.355σ as w shrinks, so the textbook half-depth
reading can't see below the blur. We invert the blurred-box model, with σ measured from the marker's edges. Where no inversion exists we
stop: a crack must span at least 4.25σ and 4 px or it is refused. With that gate turned off, the half-depth reading returns 0.31 mm for a
0.20 mm crack, and the blur-corrected estimator reads it 12% low. No estimator recovers a width the photograph doesn't contain, so the
answer is a refusal with an upper bound.
Uncertainty. Six terms in quadrature (scale, coplanarity, sampling, quantisation, estimator noise, blur calibration), reported at k = 2.
With a manual scale the scale term becomes 2 × click error / span plus 1/cos(tilt) − 1 for a tilt the operator vouches for (1.54% at the
default 10°), and blur falls back to a stated default of 0.9 px.
OpenCV 5 specifics. cv2.FontFace renders the survey annotations in a real TrueType face. VideoCapture.get now returns −1 for
unsupported properties, where 4.x returned 0, and we handle that.
AWS. An arm64 Docker image in Amazon ECR runs on a c8g.large (Graviton4) EC2 instance behind Caddy, with a Let's Encrypt certificate
for an sslip.io name; shell access goes through SSM. We chose EC2 over App Runner because COOL is an AMI that runs on c8g, m8g and r8g
instances and App Runner has no Arm option. /version prints KleidiCV (ver 26.03) as the HAL.
The COOL benchmark. The product's own per-frame pipeline on a 4K frame, 15 timed runs per arm, on five arms: the COOL AMI, the stock wheel on Graviton4, a local x86 workstation, a cost-matched x86 instance and the live demo instance. COOL against the stock wheel, both on c8g.4xlarge and run back to back:
| Workload | COOL | Stock wheel | Speedup |
|---|---|---|---|
adaptiveThreshold, 4K |
14.18 ms | 24.57 ms | 1.73× |
findContours, 4K |
43.56 ms | 43.64 ms | 1.00× |
| whole frame | 68.02 ms | 75.25 ms | 1.11× |
The speedup lands on the per-pixel kernel KleidiCV accelerates, and not on the sequential contour trace. COOL is compiled with
-mcpu=neoverse-v2, an SVE baseline, TBB and KleidiCV 0.7.0; we can't say which of those, or which change between OpenCV 4.14 and 5.0,
gives the 1.73×.
Testing. 145 tests assert measured millimetres against drawn millimetres, check that the stated uncertainty covers the error, and fire every refusal path. A calibration gate renders the printable calibration card, measures its lines, and makes the service refuse every upload if it can't reproduce them.
Challenges we ran into
The stock wheel is already accelerated. The PyPI aarch64 opencv-python wheel ships Arm's KleidiCV 26.03, so pip install on Graviton
is not an unaccelerated baseline. Every Arm comparison is against a stock wheel on the same instance family.
The COOL AMI carries OpenCV 4.14.0-pre, not 5.0. Its venvs have no cv2 unless you go through their activate script. We benchmarked
COOL's build as shipped and recorded the version difference.
A per-sample resolution gate that looked right and wasn't. Applied to individual width samples, the floor dropped the narrow samples of a crack just under it and kept the wide ones, so a 0.35 mm crack came back at 0.382 mm. Deciding resolvability once per crack moved the worst error from 13.2% to 5.6%, and coverage from 95.4% to 98.1%.
arm64 deployment. Three failures (an apt-get package missing on arm64, a Caddy config, a buildx cache manifest pulled in place of the
image) are fixed in infra/deploy.sh, with the reason in a comment.
Accomplishments that we're proud of
- A stated 95% interval that held the truth 98.1% of the time over 103 readings.
- Refusal as a first-class result: 163 declined readings, every one with a code, and refused rows kept in the CSV so a schedule never reads as a clean wall.
- A benchmark that reports the speedup per function and states the COOL AMI's OpenCV version.
What's next
- Build OpenCV 5.0.0 with COOL's flags ourselves, to separate the tuning from the version change, and measure COOL on the 2-vCPU instance the demo uses.
- Print the calibration target, photograph it, and check the printed widths. Every accuracy figure above comes from rendered targets.
- A two-view check on coplanarity, the largest assumption in the budget and one a single view can't test.
Footage credits
Real photographs, used with a manual scale:
- "Crack DSC07068.JPG" by IJD Dublin. Public domain. https://commons.wikimedia.org/wiki/File:Crack_DSC07068.JPG
- "Crack monitor in Dnipro.jpg" by Alex Blokha. CC BY-SA 4.0. https://commons.wikimedia.org/wiki/File:Crack_monitor_in_Dnipro.jpg
Real photographs used to tune detection (Wikimedia Commons unless noted):
- "Detail of vertical crack in concrete retaining wall at Medway Park Sports Centre" and "Cracked concrete retaining wall at Medway Park Sports Centre", by Sunolafjagtenben-hur. CC0 1.0.
- "Concrete cracked.jpg" by John Harvey. Public domain.
- "Cracked concrete.jpg" by Boyana.kjfg. CC BY 4.0.
- "ASR cracks concrete step barrier FHWA 2006.jpg", US Federal Highway Administration. Public domain.
- "Close view of crack in concrete on east side of highway bridge pier no. 2", US Bureau of Reclamation via the US National Archives. Public domain.
- "Cracked concrete and rebar.jpg" by Downtowngal. CC BY-SA 4.0.
- "Hairline cracks.jpg" by Prygpt. CC BY-SA 4.0.
- "Concrete wall cracking as steel reinforcing corrodes and swells 9058" and "... 9061" by JonRichfield. CC BY-SA 3.0.
- Six patches from "Concrete Crack Images for Classification" by Ç.F. Özgenel, Mendeley Data, CC BY 4.0. https://data.mendeley.com/datasets/5y9wdsg2zt/2
Everything else shown is rendered by code in the repository. The survey annotations use Barlow Condensed under the SIL Open Font License 1.1.
Log in or sign up for Devpost to join the conversation.