C library for reading analog gauge values from video frames. Designed for constrained environments — no OpenCV, no heavy dependencies. Works on embedded systems with a camera.
Python reference implementation used for algorithm prototyping: sivakov512/libgauge-reference.
- CMake 3.16+
- C99 compiler
libm(standard math library)
include(FetchContent)
FetchContent_Declare(
gauge
GIT_REPOSITORY https://github.com/sivakov512/libgauge.git
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(gauge)
target_link_libraries(your_target PRIVATE gauge)Include the headers you need:
#include "gauge.h" // calibration and measurement
#include "gauge/cv.h" // computer vision primitives
#include "gauge/utils.h" // angle utilitiesFrames are grayscale, row-major. You allocate the buffer:
uint8_t buf[WIDTH * HEIGHT];
gauge_frame_t frame = {
.buf = buf,
.width = WIDTH,
.height = HEIGHT,
};Use GAUGE_SCRATCH_SIZE(width, height) to declare the scratch buffer required by
calibration and blob extraction:
size_t scratch[GAUGE_SCRATCH_SIZE(WIDTH, HEIGHT)];Pass each frame from a static scene (no arrow movement) to build a per-pixel maximum
background. Start with bg.buf zeroed:
uint8_t bg_buf[WIDTH * HEIGHT] = {0};
gauge_frame_t bg = {.buf = bg_buf, .width = WIDTH, .height = HEIGHT};
for (size_t i = 0; i < frame_count; i++) {
gauge_err_t err = gauge_update_background(&frames[i], &bg);
if (err != GAUGE_OK) { /* handle error */ }
}Provide the first frame (arrow at minimum position) and the last frame (arrow at maximum position), along with the background:
gauge_calibration_data_t ca_data;
gauge_err_t err = gauge_calibrate_by_axis_intersection(
&first, &last, &bg, GAUGE_BINARIZATION_THRESHOLD,
scratch, GAUGE_SCRATCH_SIZE(WIDTH, HEIGHT), &ca_data);
if (err != GAUGE_OK) { /* handle error */ }ca_data.spin is GAUGE_SPIN_UNKNOWN at this point.
Pass a frame where the arrow has moved noticeably from its start position:
gauge_err_t err = gauge_calibrate_spin(
&frame, &bg, GAUGE_BINARIZATION_THRESHOLD,
scratch, GAUGE_SCRATCH_SIZE(WIDTH, HEIGHT), &ca_data);
if (err == GAUGE_ERR_SPIN_UNDETERMINED) { /* arrow hasn't moved enough, try next frame */ }Repeat with subsequent frames until GAUGE_OK is returned.
For each new frame, call:
float angle;
gauge_err_t err = gauge_scan_radial(&frame, &ca_data, GAUGE_RADIAL_SCAN_STEP, &angle);
if (err != GAUGE_OK) { /* handle error */ }Returns the arrow angle in radians, normalized to [-π, π]. Map it to a scale value
using your known angle-to-value calibration.
Accepts frames one at a time and updates bg in-place via per-pixel maximization.
Call once per frame with bg->buf zeroed on the first call.
Algorithm:
- Subtract background from first and last frames — only the arrow remains.
- Binarize each result using the provided threshold.
- Extract the largest blob from each binarized frame.
- Fit a line through each blob using principal component analysis.
- Intersect the two lines to find the rotation pivot.
- Compute the arrow length as the maximum pixel distance from pivot to blob.
ca_data_out->spin is set to GAUGE_SPIN_UNKNOWN; call gauge_calibrate_spin
afterward to determine direction.
Algorithm:
- Subtract background, binarize, extract the arrow blob.
- Compute the arrow angle relative to
ca_data->pivot. - If the angular difference from
ca_data->angle_start_radexceedsGAUGE_CALIBRATE_SPIN_MIN_ANGLE_RAD(~10°), write the spin direction and returnGAUGE_OK. Otherwise returnGAUGE_ERR_SPIN_UNDETERMINED.
Algorithm:
Scans radially from angle_start_rad to angle_end_rad in steps of
radial_scan_step. At each angle, walks from the pivot along the radial direction
for arrow_len pixels, accumulating 255 - pixel_value as a score. The angle with
the highest score is written to angle_out, normalized to [-π, π].
Default step: GAUGE_RADIAL_SCAN_STEP (~0.5°). Smaller steps give higher angular
resolution at the cost of more computation.