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 0000000000..ea0573806d Binary files /dev/null and b/samples/data/gauge-1.jpg differ