gauge.cpp 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /**
  2. @file gauge.cpp
  3. @author Alessandro de Oliveira Faria (A.K.A. CABELO)
  4. @brief This sample application processes an image frame of an analog gauge
  5. and extracts its reading using functions from the OpenCV* computer vision
  6. library. The workflow is divided into two stages: calibration and
  7. measurement. Questions and suggestions email to:
  8. Alessandro de Oliveira Faria cabelo[at]opensuse[dot]org or OpenCV Team.
  9. @date Nov 26, 2025
  10. */
  11. #include "opencv2/imgproc.hpp"
  12. #include <opencv2/highgui.hpp>
  13. #include <iostream>
  14. #include <vector>
  15. #include <cmath>
  16. #include <string>
  17. using namespace cv;
  18. using namespace std;
  19. struct GaugeCalibration {
  20. double min_angle;
  21. double max_angle;
  22. double min_value;
  23. double max_value;
  24. string units;
  25. int x;
  26. int y;
  27. int r;
  28. };
  29. static Vec3f avg_circles(const vector<Vec3f> &circles) {
  30. double avg_x = 0.0;
  31. double avg_y = 0.0;
  32. double avg_r = 0.0;
  33. int b = static_cast<int>(circles.size());
  34. for (int i = 0; i < b; ++i) {
  35. avg_x += circles[i][0];
  36. avg_y += circles[i][1];
  37. avg_r += circles[i][2];
  38. }
  39. avg_x /= b;
  40. avg_y /= b;
  41. avg_r /= b;
  42. return Vec3f(static_cast<float>(avg_x),
  43. static_cast<float>(avg_y),
  44. static_cast<float>(avg_r));
  45. }
  46. static double dist_2_pts(double x1, double y1, double x2, double y2) {
  47. double dx = x2 - x1;
  48. double dy = y2 - y1;
  49. return std::sqrt(dx * dx + dy * dy);
  50. }
  51. static GaugeCalibration calibrate_gauge(string filename) {
  52. GaugeCalibration cal;
  53. Mat img = imread(filename);
  54. if (img.empty()) {
  55. cerr << "Error loading image: " << filename << endl;
  56. exit(1);
  57. }
  58. int height = img.rows;
  59. Mat gray;
  60. cvtColor(img, gray, COLOR_BGR2GRAY);
  61. // Detects circles (HoughCircles)
  62. vector<Vec3f> circles;
  63. HoughCircles(gray, circles, HOUGH_GRADIENT, 1,
  64. 20,
  65. 100, 50,
  66. static_cast<int>(height * 0.35),
  67. static_cast<int>(height * 0.48));
  68. if (circles.empty()) {
  69. cerr << "No circles found." << endl;
  70. exit(1);
  71. }
  72. Vec3f avg = avg_circles(circles);
  73. int x = static_cast<int>(avg[0]);
  74. int y = static_cast<int>(avg[1]);
  75. int r = static_cast<int>(avg[2]);
  76. cal.x = x;
  77. cal.y = y;
  78. cal.r = r;
  79. // Draw the circle and the center.
  80. circle(img, Point(x, y), r, Scalar(0, 0, 255), 3, LINE_AA);
  81. circle(img, Point(x, y), 2, Scalar(0, 255, 0), 3, LINE_AA);
  82. // Generation of calibration lines.
  83. double separation = 10.0; // in degrees
  84. int interval = static_cast<int>(360.0 / separation);
  85. vector<Point2d> p1(interval);
  86. vector<Point2d> p2(interval);
  87. vector<Point2d> p_text(interval);
  88. for (int i = 0; i < interval; ++i) {
  89. double angle_rad = separation * i * CV_PI / 180.0;
  90. p1[i].x = x + 0.9 * r * std::cos(angle_rad);
  91. p1[i].y = y + 0.9 * r * std::sin(angle_rad);
  92. }
  93. int text_offset_x = 10;
  94. int text_offset_y = 5;
  95. for (int i = 0; i < interval; ++i) {
  96. double angle_rad = separation * i * CV_PI / 180.0;
  97. p2[i].x = x + r * std::cos(angle_rad);
  98. p2[i].y = y + r * std::sin(angle_rad);
  99. double text_angle_rad = separation * (i + 9) * CV_PI / 180.0; // i+9 = rotate 90°
  100. p_text[i].x = x - text_offset_x + 1.2 * r * std::cos(text_angle_rad);
  101. p_text[i].y = y + text_offset_y + 1.2 * r * std::sin(text_angle_rad);
  102. }
  103. // Draws lines and text.
  104. for (int i = 0; i < interval; ++i) {
  105. line(img,
  106. Point(static_cast<int>(p1[i].x), static_cast<int>(p1[i].y)),
  107. Point(static_cast<int>(p2[i].x), static_cast<int>(p2[i].y)),
  108. Scalar(0, 255, 0), 2);
  109. putText(img,
  110. to_string(static_cast<int>(i * separation)),
  111. Point(static_cast<int>(p_text[i].x), static_cast<int>(p_text[i].y)),
  112. FONT_HERSHEY_SIMPLEX, 0.3, Scalar(0, 0, 0), 1, LINE_AA);
  113. }
  114. // Save calibration image
  115. imwrite("gauge-calibration.jpg", img);
  116. cout << "Min angle (lowest possible angle of dial) - in degrees: ";
  117. cin >> cal.min_angle;
  118. cout << "Max angle (highest possible angle) - in degrees: ";
  119. cin >> cal.max_angle;
  120. cout << "Min value: ";
  121. cin >> cal.min_value;
  122. cout << "Max value: ";
  123. cin >> cal.max_value;
  124. cout << "Enter units: ";
  125. cin >> cal.units;
  126. return cal;
  127. }
  128. static double get_current_value(Mat img,
  129. const GaugeCalibration &cal) {
  130. Mat gray2;
  131. cvtColor(img, gray2, COLOR_BGR2GRAY);
  132. int thresh = 175;
  133. int maxValue = 255;
  134. Mat dst2;
  135. threshold(gray2, dst2, thresh, maxValue, THRESH_BINARY_INV);
  136. // For debugging: threshold image
  137. //imwrite("gauge-tempdst2.jpg", dst2);
  138. // Detect lines
  139. vector<Vec4i> lines;
  140. int minLineLength = 10;
  141. int maxLineGap = 0;
  142. HoughLinesP(dst2, lines, 3, CV_PI / 180, 100, minLineLength, maxLineGap);
  143. if (lines.empty()) {
  144. cerr << "No rows found." << endl;
  145. return 0.0;
  146. }
  147. // Filter lines by distance from center
  148. vector<Vec4i> final_line_list;
  149. double diff1LowerBound = 0.15;
  150. double diff1UpperBound = 0.25;
  151. double diff2LowerBound = 0.5;
  152. double diff2UpperBound = 1.0;
  153. int x = cal.x;
  154. int y = cal.y;
  155. int r = cal.r;
  156. for (size_t i = 0; i < lines.size(); ++i) {
  157. int x1 = lines[i][0];
  158. int y1 = lines[i][1];
  159. int x2 = lines[i][2];
  160. int y2 = lines[i][3];
  161. double diff1 = dist_2_pts(x, y, x1, y1);
  162. double diff2 = dist_2_pts(x, y, x2, y2);
  163. // ensures that diff1 is the smallest (closest to the center)
  164. if (diff1 > diff2) {
  165. std::swap(diff1, diff2);
  166. }
  167. if ((diff1 < diff1UpperBound * r) && (diff1 > diff1LowerBound * r) &&
  168. (diff2 < diff2UpperBound * r) && (diff2 > diff2LowerBound * r)) {
  169. final_line_list.push_back(lines[i]);
  170. }
  171. }
  172. if (final_line_list.empty()) {
  173. cerr << "No lines within the expected radius." << endl;
  174. return 0.0;
  175. }
  176. // Use the first filtered line.
  177. int x1 = final_line_list[0][0];
  178. int y1 = final_line_list[0][1];
  179. int x2 = final_line_list[0][2];
  180. int y2 = final_line_list[0][3];
  181. // Draw the line on the original image for debugging.
  182. line(img, Point(x1, y1), Point(x2, y2), Scalar(0, 255, 0), 2);
  183. //imwrite("gauge-lines-2.jpg", img);
  184. // Decide which point is furthest from the center.
  185. double dist_pt_0 = dist_2_pts(x, y, x1, y1);
  186. double dist_pt_1 = dist_2_pts(x, y, x2, y2);
  187. double x_angle, y_angle;
  188. if (dist_pt_0 > dist_pt_1) {
  189. x_angle = x1 - x;
  190. y_angle = y - y1;
  191. } else {
  192. x_angle = x2 - x;
  193. y_angle = y - y2;
  194. }
  195. double res = 0;
  196. // atan(y/x) in radians
  197. if(x_angle != 0)
  198. {
  199. res = std::atan(y_angle / x_angle);
  200. res = res * 180.0 / CV_PI; // rad2deg
  201. }
  202. double final_angle = 0.0;
  203. if (x_angle > 0 && y_angle > 0) { // Quadrante I
  204. final_angle = 270.0 - res;
  205. }
  206. if (x_angle < 0 && y_angle > 0) { // Quadrante II
  207. final_angle = 90.0 - res;
  208. }
  209. if (x_angle < 0 && y_angle < 0) { // Quadrante III
  210. final_angle = 90.0 - res;
  211. }
  212. if (x_angle > 0 && y_angle < 0) { // Quadrante IV
  213. final_angle = 270.0 - res;
  214. }
  215. double old_min = cal.min_angle;
  216. double old_max = cal.max_angle;
  217. double new_min = cal.min_value;
  218. double new_max = cal.max_value;
  219. double old_value = final_angle;
  220. double old_range = (old_max - old_min);
  221. double new_range = (new_max - new_min);
  222. double new_value = (((old_value - old_min) * new_range) / old_range) + new_min;
  223. return new_value;
  224. }
  225. int main(int argc, char *argv[]) {
  226. CommandLineParser parser(argc, argv, "{@input |gauge-1.jpg|Input image to reads the value using functions from the OpenCV. }");
  227. parser.about("Analog-gauge-reader example:\n");
  228. parser.printMessage();
  229. string filename = parser.get<String>("@input");
  230. Mat img = imread(filename);
  231. if (img.empty()) {
  232. cerr << "Error loading image. " << filename << endl;
  233. return 1;
  234. }
  235. // Calibration
  236. GaugeCalibration cal = calibrate_gauge(filename);
  237. double val = get_current_value(img, cal );
  238. cout << "Current reading: " << val << " " << cal.units << endl;
  239. return 0;
  240. }