blob: 96d7ffdeb2bc48dd52a49d5716137540308b5fa2 [file] [log] [blame]
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -08001#include "y2022/vision/blob_detector.h"
2
milind-u92195982022-01-22 20:29:31 -08003#include <cmath>
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -08004#include <optional>
milind-u92195982022-01-22 20:29:31 -08005#include <string>
6
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -08007#include "aos/network/team_number.h"
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -08008#include "aos/time/time.h"
milind-u92195982022-01-22 20:29:31 -08009#include "opencv2/features2d.hpp"
10#include "opencv2/imgproc.hpp"
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080011
Yash Chainani6acad6f2022-02-03 10:52:53 -080012DEFINE_uint64(red_delta, 100,
13 "Required difference between green pixels vs. red");
14DEFINE_uint64(blue_delta, 50,
15 "Required difference between green pixels vs. blue");
16
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080017DEFINE_bool(use_outdoors, false,
18 "If true, change thresholds to handle outdoor illumination");
Yash Chainani6acad6f2022-02-03 10:52:53 -080019DEFINE_uint64(outdoors_red_delta, 100,
20 "Difference between green pixels vs. red, when outdoors");
21DEFINE_uint64(outdoors_blue_delta, 15,
22 "Difference between green pixels vs. blue, when outdoors");
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080023
24namespace y2022 {
25namespace vision {
26
Milind Upadhyayec41e132022-02-05 17:14:05 -080027cv::Mat BlobDetector::ThresholdImage(cv::Mat bgr_image) {
Yash Chainani6acad6f2022-02-03 10:52:53 -080028 size_t red_delta = FLAGS_red_delta;
29 size_t blue_delta = FLAGS_blue_delta;
30
31 if (FLAGS_use_outdoors) {
32 red_delta = FLAGS_outdoors_red_delta;
33 red_delta = FLAGS_outdoors_blue_delta;
34 }
35
Milind Upadhyayec41e132022-02-05 17:14:05 -080036 cv::Mat binarized_image(cv::Size(bgr_image.cols, bgr_image.rows), CV_8UC1);
37 for (int row = 0; row < bgr_image.rows; row++) {
38 for (int col = 0; col < bgr_image.cols; col++) {
39 cv::Vec3b pixel = bgr_image.at<cv::Vec3b>(row, col);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080040 uint8_t blue = pixel.val[0];
41 uint8_t green = pixel.val[1];
42 uint8_t red = pixel.val[2];
43 // Simple filter that looks for green pixels sufficiently brigher than
44 // red and blue
Yash Chainani6acad6f2022-02-03 10:52:53 -080045 if ((green > blue + blue_delta) && (green > red + red_delta)) {
milind-u61f21e82022-01-23 18:34:11 -080046 binarized_image.at<uint8_t>(row, col) = 255;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080047 } else {
milind-u61f21e82022-01-23 18:34:11 -080048 binarized_image.at<uint8_t>(row, col) = 0;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080049 }
50 }
51 }
52
milind-u61f21e82022-01-23 18:34:11 -080053 return binarized_image;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080054}
55
56std::vector<std::vector<cv::Point>> BlobDetector::FindBlobs(
57 cv::Mat binarized_image) {
58 // find the contours (blob outlines)
59 std::vector<std::vector<cv::Point>> contours;
60 std::vector<cv::Vec4i> hierarchy;
61 cv::findContours(binarized_image, contours, hierarchy, cv::RETR_CCOMP,
62 cv::CHAIN_APPROX_SIMPLE);
63
64 return contours;
65}
66
milind-u61f21e82022-01-23 18:34:11 -080067std::vector<BlobDetector::BlobStats> BlobDetector::ComputeStats(
68 std::vector<std::vector<cv::Point>> blobs) {
69 std::vector<BlobDetector::BlobStats> blob_stats;
70 for (auto blob : blobs) {
71 // Make the blob convex before finding bounding box
72 std::vector<cv::Point> convex_blob;
73 cv::convexHull(blob, convex_blob);
74 auto blob_size = cv::boundingRect(convex_blob).size();
75 cv::Moments moments = cv::moments(convex_blob);
76
77 const auto centroid =
78 cv::Point(moments.m10 / moments.m00, moments.m01 / moments.m00);
79 const double aspect_ratio =
80 static_cast<double>(blob_size.width) / blob_size.height;
81 const double area = moments.m00;
Henry Speisere45e7a22022-02-04 23:17:01 -080082 const size_t num_points = blob.size();
milind-u61f21e82022-01-23 18:34:11 -080083
Henry Speisere45e7a22022-02-04 23:17:01 -080084 blob_stats.emplace_back(
85 BlobStats{centroid, aspect_ratio, area, num_points});
milind-u61f21e82022-01-23 18:34:11 -080086 }
87 return blob_stats;
88}
89
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -080090namespace {
91
92// Linear equation in the form ax + by = c
93struct Line {
94 public:
95 double a, b, c;
96
97 std::optional<cv::Point2d> Intersection(const Line &l) const {
98 // Use Cramer's rule to solve for the intersection
99 const double denominator = Determinant(a, b, l.a, l.b);
100 const double numerator_x = Determinant(c, b, l.c, l.b);
101 const double numerator_y = Determinant(a, c, l.a, l.c);
102
103 std::optional<cv::Point2d> intersection = std::nullopt;
104 // Return nullopt if the denominator is 0, meaning the same slopes
105 if (denominator != 0) {
106 intersection =
107 cv::Point2d(numerator_x / denominator, numerator_y / denominator);
108 }
109
110 return intersection;
111 }
112
113 private: // Determinant of [[a, b], [c, d]]
114 static double Determinant(double a, double b, double c, double d) {
115 return (a * d) - (b * c);
116 }
117};
118
119struct Circle {
120 public:
121 cv::Point2d center;
122 double radius;
123
124 static std::optional<Circle> Fit(std::vector<cv::Point2d> centroids) {
125 CHECK_EQ(centroids.size(), 3ul);
126 // For the 3 points, we have 3 equations in the form
127 // (x - h)^2 + (y - k)^2 = r^2
128 // Manipulate them to solve for the center and radius
129 // (x1 - h)^2 + (y1 - k)^2 = r^2 ->
130 // x1^2 + h^2 - 2x1h + y1^2 + k^2 - 2y1k = r^2
131 // Also, (x2 - h)^2 + (y2 - k)^2 = r^2
132 // Subtracting these two, we get
133 // x1^2 - x2^2 - 2h(x1 - x2) + y1^2 - y2^2 - 2k(y1 - y2) = 0 ->
134 // h(x1 - x2) + k(y1 - y2) = (-x1^2 + x2^2 - y1^2 + y2^2) / -2
135 // Doing the same with equations 1 and 3, we get the second linear equation
136 // h(x1 - x3) + k(y1 - y3) = (-x1^2 + x3^2 - y1^2 + y3^2) / -2
137 // Now, we can solve for their intersection and find the center
138 const auto l =
139 Line{centroids[0].x - centroids[1].x, centroids[0].y - centroids[1].y,
140 (-std::pow(centroids[0].x, 2) + std::pow(centroids[1].x, 2) -
141 std::pow(centroids[0].y, 2) + std::pow(centroids[1].y, 2)) /
142 -2.0};
143 const auto m =
144 Line{centroids[0].x - centroids[2].x, centroids[0].y - centroids[2].y,
145 (-std::pow(centroids[0].x, 2) + std::pow(centroids[2].x, 2) -
146 std::pow(centroids[0].y, 2) + std::pow(centroids[2].y, 2)) /
147 -2.0};
148 const auto center = l.Intersection(m);
149
150 std::optional<Circle> circle = std::nullopt;
151 if (center) {
152 // Now find the radius
153 const double radius = cv::norm(centroids[0] - *center);
154 circle = Circle{*center, radius};
155 }
156 return circle;
157 }
158
159 double DistanceTo(cv::Point2d p) const {
Milind Upadhyayec41e132022-02-05 17:14:05 -0800160 const auto p_prime = TranslateToOrigin(p);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800161 // Now, the distance is simply the difference between distance from the
162 // origin to p' and the radius.
163 return std::abs(cv::norm(p_prime) - radius);
164 }
165
Milind Upadhyayec41e132022-02-05 17:14:05 -0800166 bool InAngleRange(cv::Point2d p, double theta_min, double theta_max) const {
167 auto p_prime = TranslateToOrigin(p);
168 // Flip the y because y values go downwards.
169 p_prime.y *= -1;
170 const double theta = std::atan2(p_prime.y, p_prime.x);
171 return (theta >= theta_min && theta <= theta_max);
172 }
173
174 private:
175 // Translate the point on the circle
176 // as if the circle's center is the origin (0,0)
177 cv::Point2d TranslateToOrigin(cv::Point2d p) const {
178 return cv::Point2d(p.x - center.x, p.y - center.y);
179 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800180};
181
182} // namespace
183
184std::pair<std::vector<std::vector<cv::Point>>, cv::Point>
185BlobDetector::FilterBlobs(std::vector<std::vector<cv::Point>> blobs,
186 std::vector<BlobDetector::BlobStats> blob_stats) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800187 std::vector<std::vector<cv::Point>> filtered_blobs;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800188 std::vector<BlobStats> filtered_stats;
milind-u92195982022-01-22 20:29:31 -0800189
milind-u61f21e82022-01-23 18:34:11 -0800190 auto blob_it = blobs.begin();
191 auto stats_it = blob_stats.begin();
192 while (blob_it < blobs.end() && stats_it < blob_stats.end()) {
milind-u92195982022-01-22 20:29:31 -0800193 // To estimate the maximum y, we can figure out the y value of the blobs
194 // when the camera is the farthest from the target, at the field corner.
195 // We can solve for the pitch of the blob:
196 // blob_pitch = atan((height_tape - height_camera) / depth) + camera_pitch
Yash Chainani6acad6f2022-02-03 10:52:53 -0800197 // The triangle with the height of the tape above the camera and the
198 // camera depth is similar to the one with the focal length in y pixels
199 // and the y coordinate offset from the center of the image. Therefore
200 // y_offset = focal_length_y * tan(blob_pitch), and y = -(y_offset -
201 // offset_y)
milind-u61f21e82022-01-23 18:34:11 -0800202 constexpr int kMaxY = 400;
203 constexpr double kTapeAspectRatio = 5.0 / 2.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800204 constexpr double kAspectRatioThreshold = 1.6;
milind-u61f21e82022-01-23 18:34:11 -0800205 constexpr double kMinArea = 10;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800206 constexpr size_t kMinNumPoints = 6;
milind-u92195982022-01-22 20:29:31 -0800207
milind-u61f21e82022-01-23 18:34:11 -0800208 // Remove all blobs that are at the bottom of the image, have a different
Milind Upadhyayec41e132022-02-05 17:14:05 -0800209 // aspect ratio than the tape, or have too little area or points.
milind-u61f21e82022-01-23 18:34:11 -0800210 if ((stats_it->centroid.y <= kMaxY) &&
211 (std::abs(kTapeAspectRatio - stats_it->aspect_ratio) <
212 kAspectRatioThreshold) &&
Milind Upadhyayec41e132022-02-05 17:14:05 -0800213 (stats_it->area >= kMinArea) &&
214 (stats_it->num_points >= kMinNumPoints)) {
milind-u61f21e82022-01-23 18:34:11 -0800215 filtered_blobs.push_back(*blob_it);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800216 filtered_stats.push_back(*stats_it);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800217 }
milind-u61f21e82022-01-23 18:34:11 -0800218 blob_it++;
219 stats_it++;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800220 }
milind-u92195982022-01-22 20:29:31 -0800221
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800222 // Threshold for mean distance from a blob centroid to a circle.
223 constexpr double kCircleDistanceThreshold = 5.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800224 // We should only expect to see blobs between these angles on a circle.
225 constexpr double kMinBlobAngle = M_PI / 3;
226 constexpr double kMaxBlobAngle = M_PI - kMinBlobAngle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800227 std::vector<std::vector<cv::Point>> blob_circle;
228 std::vector<cv::Point2d> centroids;
229
230 // If we see more than this number of blobs after filtering based on
231 // color/size, the circle fit may detect noise so just return no blobs.
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800232 constexpr size_t kMinFilteredBlobs = 3;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800233 constexpr size_t kMaxFilteredBlobs = 50;
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800234 if (filtered_blobs.size() >= kMinFilteredBlobs &&
235 filtered_blobs.size() <= kMaxFilteredBlobs) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800236 constexpr size_t kRansacIterations = 15;
237 for (size_t i = 0; i < kRansacIterations; i++) {
238 // Pick 3 random blobs and see how many fit on their circle
239 const size_t j = std::rand() % filtered_blobs.size();
240 const size_t k = std::rand() % filtered_blobs.size();
241 const size_t l = std::rand() % filtered_blobs.size();
242
243 // Restart if the random indices clash
244 if ((j == k) || (j == l) || (k == l)) {
245 i--;
246 continue;
247 }
248
249 std::vector<std::vector<cv::Point>> current_blobs{
250 filtered_blobs[j], filtered_blobs[k], filtered_blobs[l]};
251 std::vector<cv::Point2d> current_centroids{filtered_stats[j].centroid,
252 filtered_stats[k].centroid,
253 filtered_stats[l].centroid};
254 const std::optional<Circle> circle = Circle::Fit(current_centroids);
255
256 // Make sure that a circle could be created from the points
257 if (!circle) {
258 continue;
259 }
260
Milind Upadhyayec41e132022-02-05 17:14:05 -0800261 // Only try to fit points to this circle if all of these are between
262 // certain angles.
263 if (circle->InAngleRange(current_centroids[0], kMinBlobAngle,
264 kMaxBlobAngle) &&
265 circle->InAngleRange(current_centroids[1], kMinBlobAngle,
266 kMaxBlobAngle) &&
267 circle->InAngleRange(current_centroids[2], kMinBlobAngle,
268 kMaxBlobAngle)) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800269 for (size_t m = 0; m < filtered_blobs.size(); m++) {
270 // Add this blob to the list if it is close to the circle, is on the
271 // top half, and isn't one of the other blobs
272 if ((m != i) && (m != j) && (m != k) &&
Milind Upadhyayec41e132022-02-05 17:14:05 -0800273 circle->InAngleRange(filtered_stats[m].centroid, kMinBlobAngle,
274 kMaxBlobAngle) &&
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800275 (circle->DistanceTo(filtered_stats[m].centroid) <
276 kCircleDistanceThreshold)) {
277 current_blobs.emplace_back(filtered_blobs[m]);
278 current_centroids.emplace_back(filtered_stats[m].centroid);
279 }
280 }
281
282 if (current_blobs.size() > blob_circle.size()) {
283 blob_circle = current_blobs;
284 centroids = current_centroids;
285 }
286 }
287 }
288 }
289
290 cv::Point avg_centroid(-1, -1);
291 if (centroids.size() > 0) {
292 for (auto centroid : centroids) {
293 avg_centroid.x += centroid.x;
294 avg_centroid.y += centroid.y;
295 }
296 avg_centroid.x /= centroids.size();
297 avg_centroid.y /= centroids.size();
298 }
299
300 return {blob_circle, avg_centroid};
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800301}
302
303void BlobDetector::DrawBlobs(
milind-u61f21e82022-01-23 18:34:11 -0800304 cv::Mat view_image,
305 const std::vector<std::vector<cv::Point>> &unfiltered_blobs,
306 const std::vector<std::vector<cv::Point>> &filtered_blobs,
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800307 const std::vector<BlobStats> &blob_stats, cv::Point centroid) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800308 CHECK_GT(view_image.cols, 0);
309 if (unfiltered_blobs.size() > 0) {
310 // Draw blobs unfilled, with red color border
milind-u92195982022-01-22 20:29:31 -0800311 cv::drawContours(view_image, unfiltered_blobs, -1, cv::Scalar(0, 0, 255),
312 0);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800313 }
314
milind-u92195982022-01-22 20:29:31 -0800315 cv::drawContours(view_image, filtered_blobs, -1, cv::Scalar(0, 255, 0),
316 cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800317
milind-u92195982022-01-22 20:29:31 -0800318 // Draw blob centroids
milind-u61f21e82022-01-23 18:34:11 -0800319 for (auto stats : blob_stats) {
320 cv::circle(view_image, stats.centroid, 2, cv::Scalar(255, 0, 0),
321 cv::FILLED);
322 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800323
324 // Draw average centroid
325 cv::circle(view_image, centroid, 3, cv::Scalar(255, 255, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800326}
327
Milind Upadhyayec41e132022-02-05 17:14:05 -0800328void BlobDetector::ExtractBlobs(cv::Mat bgr_image,
Milind Upadhyay25610d22022-02-07 15:35:26 -0800329 BlobDetector::BlobResult *blob_result) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800330 auto start = aos::monotonic_clock::now();
Milind Upadhyayec41e132022-02-05 17:14:05 -0800331 blob_result->binarized_image = ThresholdImage(bgr_image);
Milind Upadhyay25610d22022-02-07 15:35:26 -0800332 blob_result->unfiltered_blobs = FindBlobs(blob_result->binarized_image);
333 blob_result->blob_stats = ComputeStats(blob_result->unfiltered_blobs);
334 auto filtered_pair =
335 FilterBlobs(blob_result->unfiltered_blobs, blob_result->blob_stats);
336 blob_result->filtered_blobs = filtered_pair.first;
337 blob_result->centroid = filtered_pair.second;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800338 auto end = aos::monotonic_clock::now();
Jim Ostrowskifec0c332022-02-06 23:28:26 -0800339 VLOG(2) << "Blob detection elapsed time: "
340 << std::chrono::duration<double, std::milli>(end - start).count()
341 << " ms";
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800342}
343
344} // namespace vision
345} // namespace y2022