From 9c3d3dbee7ffa4b68ca0dd4ec6ab246ac021637d Mon Sep 17 00:00:00 2001 From: "Alessandro de Oliveira Faria (A.K.A.CABELO)" Date: Wed, 3 Dec 2025 04:05:50 -0300 Subject: [PATCH] Merge pull request #28082 from cabelo:4.x Analog Gauge Reader - cabelo@opensuse.org #28082 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake I humbly offer this example so that all developers can understand that many problems can be solved without VLM models. image image image --- samples/cpp/gauge.cpp | 300 +++++++++++++++++++++++++++++++++++++++ samples/data/gauge-1.jpg | Bin 0 -> 17297 bytes 2 files changed, 300 insertions(+) create mode 100644 samples/cpp/gauge.cpp create mode 100644 samples/data/gauge-1.jpg diff --git a/samples/cpp/gauge.cpp b/samples/cpp/gauge.cpp new file mode 100644 index 0000000000..e3a7dcfe92 --- /dev/null +++ b/samples/cpp/gauge.cpp @@ -0,0 +1,300 @@ +/** + @file gauge.cpp + @author Alessandro de Oliveira Faria (A.K.A. CABELO) + @brief This sample application processes an image frame of an analog gauge + and extracts its reading using functions from the OpenCV* computer vision + library. The workflow is divided into two stages: calibration and + measurement. Questions and suggestions email to: + Alessandro de Oliveira Faria cabelo[at]opensuse[dot]org or OpenCV Team. + @date Nov 26, 2025 +*/ + +#include "opencv2/imgproc.hpp" +#include +#include +#include +#include +#include + +using namespace cv; +using namespace std; + +struct GaugeCalibration { + double min_angle; + double max_angle; + double min_value; + double max_value; + string units; + int x; + int y; + int r; +}; + +static Vec3f avg_circles(const vector &circles) { + double avg_x = 0.0; + double avg_y = 0.0; + double avg_r = 0.0; + + int b = static_cast(circles.size()); + for (int i = 0; i < b; ++i) { + avg_x += circles[i][0]; + avg_y += circles[i][1]; + avg_r += circles[i][2]; + } + + avg_x /= b; + avg_y /= b; + avg_r /= b; + + return Vec3f(static_cast(avg_x), + static_cast(avg_y), + static_cast(avg_r)); +} + +static double dist_2_pts(double x1, double y1, double x2, double y2) { + double dx = x2 - x1; + double dy = y2 - y1; + return std::sqrt(dx * dx + dy * dy); +} + +static GaugeCalibration calibrate_gauge(string filename) { + GaugeCalibration cal; + Mat img = imread(filename); + if (img.empty()) { + cerr << "Error loading image: " << filename << endl; + exit(1); + } + + int height = img.rows; + + Mat gray; + cvtColor(img, gray, COLOR_BGR2GRAY); + + // Detects circles (HoughCircles) + vector circles; + HoughCircles(gray, circles, HOUGH_GRADIENT, 1, + 20, + 100, 50, + static_cast(height * 0.35), + static_cast(height * 0.48)); + + if (circles.empty()) { + cerr << "No circles found." << endl; + exit(1); + } + + Vec3f avg = avg_circles(circles); + int x = static_cast(avg[0]); + int y = static_cast(avg[1]); + int r = static_cast(avg[2]); + + cal.x = x; + cal.y = y; + cal.r = r; + + // Draw the circle and the center. + circle(img, Point(x, y), r, Scalar(0, 0, 255), 3, LINE_AA); + circle(img, Point(x, y), 2, Scalar(0, 255, 0), 3, LINE_AA); + + // Generation of calibration lines. + double separation = 10.0; // in degrees + int interval = static_cast(360.0 / separation); + + vector p1(interval); + vector p2(interval); + vector p_text(interval); + + for (int i = 0; i < interval; ++i) { + double angle_rad = separation * i * CV_PI / 180.0; + p1[i].x = x + 0.9 * r * std::cos(angle_rad); + p1[i].y = y + 0.9 * r * std::sin(angle_rad); + } + + int text_offset_x = 10; + int text_offset_y = 5; + + for (int i = 0; i < interval; ++i) { + double angle_rad = separation * i * CV_PI / 180.0; + p2[i].x = x + r * std::cos(angle_rad); + p2[i].y = y + r * std::sin(angle_rad); + + double text_angle_rad = separation * (i + 9) * CV_PI / 180.0; // i+9 = rotate 90° + p_text[i].x = x - text_offset_x + 1.2 * r * std::cos(text_angle_rad); + p_text[i].y = y + text_offset_y + 1.2 * r * std::sin(text_angle_rad); + } + + // Draws lines and text. + for (int i = 0; i < interval; ++i) { + line(img, + Point(static_cast(p1[i].x), static_cast(p1[i].y)), + Point(static_cast(p2[i].x), static_cast(p2[i].y)), + Scalar(0, 255, 0), 2); + + putText(img, + to_string(static_cast(i * separation)), + Point(static_cast(p_text[i].x), static_cast(p_text[i].y)), + FONT_HERSHEY_SIMPLEX, 0.3, Scalar(0, 0, 0), 1, LINE_AA); + } + + // Save calibration image + imwrite("gauge-calibration.jpg", img); + + cout << "Min angle (lowest possible angle of dial) - in degrees: "; + cin >> cal.min_angle; + + cout << "Max angle (highest possible angle) - in degrees: "; + cin >> cal.max_angle; + + cout << "Min value: "; + cin >> cal.min_value; + + cout << "Max value: "; + cin >> cal.max_value; + + cout << "Enter units: "; + cin >> cal.units; + + return cal; +} + +static double get_current_value(Mat img, + const GaugeCalibration &cal) { + Mat gray2; + cvtColor(img, gray2, COLOR_BGR2GRAY); + + int thresh = 175; + int maxValue = 255; + + Mat dst2; + threshold(gray2, dst2, thresh, maxValue, THRESH_BINARY_INV); + + // For debugging: threshold image + //imwrite("gauge-tempdst2.jpg", dst2); + + // Detect lines + vector lines; + int minLineLength = 10; + int maxLineGap = 0; + HoughLinesP(dst2, lines, 3, CV_PI / 180, 100, minLineLength, maxLineGap); + + if (lines.empty()) { + cerr << "No rows found." << endl; + return 0.0; + } + + // Filter lines by distance from center + vector final_line_list; + + double diff1LowerBound = 0.15; + double diff1UpperBound = 0.25; + double diff2LowerBound = 0.5; + double diff2UpperBound = 1.0; + + int x = cal.x; + int y = cal.y; + int r = cal.r; + + for (size_t i = 0; i < lines.size(); ++i) { + int x1 = lines[i][0]; + int y1 = lines[i][1]; + int x2 = lines[i][2]; + int y2 = lines[i][3]; + + double diff1 = dist_2_pts(x, y, x1, y1); + double diff2 = dist_2_pts(x, y, x2, y2); + + // ensures that diff1 is the smallest (closest to the center) + if (diff1 > diff2) { + std::swap(diff1, diff2); + } + + if ((diff1 < diff1UpperBound * r) && (diff1 > diff1LowerBound * r) && + (diff2 < diff2UpperBound * r) && (diff2 > diff2LowerBound * r)) { + final_line_list.push_back(lines[i]); + } + } + + if (final_line_list.empty()) { + cerr << "No lines within the expected radius." << endl; + return 0.0; + } + + // Use the first filtered line. + int x1 = final_line_list[0][0]; + int y1 = final_line_list[0][1]; + int x2 = final_line_list[0][2]; + int y2 = final_line_list[0][3]; + + // Draw the line on the original image for debugging. + line(img, Point(x1, y1), Point(x2, y2), Scalar(0, 255, 0), 2); + //imwrite("gauge-lines-2.jpg", img); + + // Decide which point is furthest from the center. + double dist_pt_0 = dist_2_pts(x, y, x1, y1); + double dist_pt_1 = dist_2_pts(x, y, x2, y2); + + double x_angle, y_angle; + if (dist_pt_0 > dist_pt_1) { + x_angle = x1 - x; + y_angle = y - y1; + } else { + x_angle = x2 - x; + y_angle = y - y2; + } + + double res = 0; + // atan(y/x) in radians + if(x_angle != 0) + { + res = std::atan(y_angle / x_angle); + res = res * 180.0 / CV_PI; // rad2deg + } + double final_angle = 0.0; + + if (x_angle > 0 && y_angle > 0) { // Quadrante I + final_angle = 270.0 - res; + } + if (x_angle < 0 && y_angle > 0) { // Quadrante II + final_angle = 90.0 - res; + } + if (x_angle < 0 && y_angle < 0) { // Quadrante III + final_angle = 90.0 - res; + } + if (x_angle > 0 && y_angle < 0) { // Quadrante IV + final_angle = 270.0 - res; + } + + double old_min = cal.min_angle; + double old_max = cal.max_angle; + double new_min = cal.min_value; + double new_max = cal.max_value; + + double old_value = final_angle; + double old_range = (old_max - old_min); + double new_range = (new_max - new_min); + + double new_value = (((old_value - old_min) * new_range) / old_range) + new_min; + + return new_value; +} + +int main(int argc, char *argv[]) { + CommandLineParser parser(argc, argv, "{@input |gauge-1.jpg|Input image to reads the value using functions from the OpenCV. }"); + parser.about("Analog-gauge-reader example:\n"); + parser.printMessage(); + string filename = parser.get("@input"); + + Mat img = imread(filename); + if (img.empty()) { + cerr << "Error loading image. " << filename << endl; + return 1; + } + + // Calibration + GaugeCalibration cal = calibrate_gauge(filename); + + double val = get_current_value(img, cal ); + cout << "Current reading: " << val << " " << cal.units << endl; + + return 0; +} diff --git a/samples/data/gauge-1.jpg b/samples/data/gauge-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea0573806da00e1560e43dbc2722d42cda2b035b GIT binary patch literal 17297 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 literal 0 HcmV?d00001