blob: 2c68a271cb9dc5e6190a27bdae47e840c0a59260 [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");
Milind Upadhyayf61e1482022-02-11 20:42:55 -080021DEFINE_uint64(outdoors_blue_delta, 1,
Yash Chainani6acad6f2022-02-03 10:52:53 -080022 "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;
Milind Upadhyayf61e1482022-02-11 20:42:55 -080033 blue_delta = FLAGS_outdoors_blue_delta;
Yash Chainani6acad6f2022-02-03 10:52:53 -080034 }
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(
Milind Upadhyayf61e1482022-02-11 20:42:55 -080068 const std::vector<std::vector<cv::Point>> &blobs) {
milind-u61f21e82022-01-23 18:34:11 -080069 std::vector<BlobDetector::BlobStats> blob_stats;
70 for (auto blob : blobs) {
Milind Upadhyayf61e1482022-02-11 20:42:55 -080071 auto blob_size = cv::boundingRect(blob).size();
72 cv::Moments moments = cv::moments(blob);
milind-u61f21e82022-01-23 18:34:11 -080073
74 const auto centroid =
75 cv::Point(moments.m10 / moments.m00, moments.m01 / moments.m00);
76 const double aspect_ratio =
77 static_cast<double>(blob_size.width) / blob_size.height;
78 const double area = moments.m00;
Henry Speisere45e7a22022-02-04 23:17:01 -080079 const size_t num_points = blob.size();
milind-u61f21e82022-01-23 18:34:11 -080080
Henry Speisere45e7a22022-02-04 23:17:01 -080081 blob_stats.emplace_back(
82 BlobStats{centroid, aspect_ratio, area, num_points});
milind-u61f21e82022-01-23 18:34:11 -080083 }
84 return blob_stats;
85}
86
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -080087namespace {
88
89// Linear equation in the form ax + by = c
90struct Line {
91 public:
92 double a, b, c;
93
94 std::optional<cv::Point2d> Intersection(const Line &l) const {
95 // Use Cramer's rule to solve for the intersection
96 const double denominator = Determinant(a, b, l.a, l.b);
97 const double numerator_x = Determinant(c, b, l.c, l.b);
98 const double numerator_y = Determinant(a, c, l.a, l.c);
99
100 std::optional<cv::Point2d> intersection = std::nullopt;
101 // Return nullopt if the denominator is 0, meaning the same slopes
102 if (denominator != 0) {
103 intersection =
104 cv::Point2d(numerator_x / denominator, numerator_y / denominator);
105 }
106
107 return intersection;
108 }
109
110 private: // Determinant of [[a, b], [c, d]]
111 static double Determinant(double a, double b, double c, double d) {
112 return (a * d) - (b * c);
113 }
114};
115
116struct Circle {
117 public:
118 cv::Point2d center;
119 double radius;
120
121 static std::optional<Circle> Fit(std::vector<cv::Point2d> centroids) {
122 CHECK_EQ(centroids.size(), 3ul);
123 // For the 3 points, we have 3 equations in the form
124 // (x - h)^2 + (y - k)^2 = r^2
125 // Manipulate them to solve for the center and radius
126 // (x1 - h)^2 + (y1 - k)^2 = r^2 ->
127 // x1^2 + h^2 - 2x1h + y1^2 + k^2 - 2y1k = r^2
128 // Also, (x2 - h)^2 + (y2 - k)^2 = r^2
129 // Subtracting these two, we get
130 // x1^2 - x2^2 - 2h(x1 - x2) + y1^2 - y2^2 - 2k(y1 - y2) = 0 ->
131 // h(x1 - x2) + k(y1 - y2) = (-x1^2 + x2^2 - y1^2 + y2^2) / -2
132 // Doing the same with equations 1 and 3, we get the second linear equation
133 // h(x1 - x3) + k(y1 - y3) = (-x1^2 + x3^2 - y1^2 + y3^2) / -2
134 // Now, we can solve for their intersection and find the center
135 const auto l =
136 Line{centroids[0].x - centroids[1].x, centroids[0].y - centroids[1].y,
137 (-std::pow(centroids[0].x, 2) + std::pow(centroids[1].x, 2) -
138 std::pow(centroids[0].y, 2) + std::pow(centroids[1].y, 2)) /
139 -2.0};
140 const auto m =
141 Line{centroids[0].x - centroids[2].x, centroids[0].y - centroids[2].y,
142 (-std::pow(centroids[0].x, 2) + std::pow(centroids[2].x, 2) -
143 std::pow(centroids[0].y, 2) + std::pow(centroids[2].y, 2)) /
144 -2.0};
145 const auto center = l.Intersection(m);
146
147 std::optional<Circle> circle = std::nullopt;
148 if (center) {
149 // Now find the radius
150 const double radius = cv::norm(centroids[0] - *center);
151 circle = Circle{*center, radius};
152 }
153 return circle;
154 }
155
156 double DistanceTo(cv::Point2d p) const {
Milind Upadhyayec41e132022-02-05 17:14:05 -0800157 const auto p_prime = TranslateToOrigin(p);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800158 // Now, the distance is simply the difference between distance from the
159 // origin to p' and the radius.
160 return std::abs(cv::norm(p_prime) - radius);
161 }
162
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800163 double AngleOf(cv::Point2d p) const {
Milind Upadhyayec41e132022-02-05 17:14:05 -0800164 auto p_prime = TranslateToOrigin(p);
165 // Flip the y because y values go downwards.
166 p_prime.y *= -1;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800167 return std::atan2(p_prime.y, p_prime.x);
168 }
169
170 // TODO(milind): handle wrapping around 2pi
171 bool InAngleRange(cv::Point2d p, double theta_min, double theta_max) const {
172 const double theta = AngleOf(p);
Milind Upadhyayec41e132022-02-05 17:14:05 -0800173 return (theta >= theta_min && theta <= theta_max);
174 }
175
176 private:
177 // Translate the point on the circle
178 // as if the circle's center is the origin (0,0)
179 cv::Point2d TranslateToOrigin(cv::Point2d p) const {
180 return cv::Point2d(p.x - center.x, p.y - center.y);
181 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800182};
183
184} // namespace
185
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800186void BlobDetector::FilterBlobs(BlobResult *blob_result) {
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 Upadhyayf61e1482022-02-11 20:42:55 -0800190 auto blob_it = blob_result->unfiltered_blobs.begin();
191 auto stats_it = blob_result->blob_stats.begin();
192 while (blob_it < blob_result->unfiltered_blobs.end() &&
193 stats_it < blob_result->blob_stats.end()) {
milind-u61f21e82022-01-23 18:34:11 -0800194 constexpr double kTapeAspectRatio = 5.0 / 2.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800195 constexpr double kAspectRatioThreshold = 1.6;
milind-u61f21e82022-01-23 18:34:11 -0800196 constexpr double kMinArea = 10;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800197 constexpr size_t kMinNumPoints = 6;
milind-u92195982022-01-22 20:29:31 -0800198
milind-u61f21e82022-01-23 18:34:11 -0800199 // Remove all blobs that are at the bottom of the image, have a different
Milind Upadhyayec41e132022-02-05 17:14:05 -0800200 // aspect ratio than the tape, or have too little area or points.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800201 if ((std::abs(1.0 - kTapeAspectRatio / stats_it->aspect_ratio) <
milind-u61f21e82022-01-23 18:34:11 -0800202 kAspectRatioThreshold) &&
Milind Upadhyayec41e132022-02-05 17:14:05 -0800203 (stats_it->area >= kMinArea) &&
204 (stats_it->num_points >= kMinNumPoints)) {
milind-u61f21e82022-01-23 18:34:11 -0800205 filtered_blobs.push_back(*blob_it);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800206 filtered_stats.push_back(*stats_it);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800207 }
milind-u61f21e82022-01-23 18:34:11 -0800208 blob_it++;
209 stats_it++;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800210 }
milind-u92195982022-01-22 20:29:31 -0800211
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800212 // Threshold for mean distance from a blob centroid to a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800213 constexpr double kCircleDistanceThreshold = 10.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800214 // We should only expect to see blobs between these angles on a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800215 constexpr double kDegToRad = M_PI / 180.0;
216 constexpr double kMinBlobAngle = 50.0 * kDegToRad;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800217 constexpr double kMaxBlobAngle = M_PI - kMinBlobAngle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800218 std::vector<std::vector<cv::Point>> blob_circle;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800219 Circle circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800220 std::vector<cv::Point2d> centroids;
221
222 // If we see more than this number of blobs after filtering based on
223 // color/size, the circle fit may detect noise so just return no blobs.
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800224 constexpr size_t kMinFilteredBlobs = 3;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800225 constexpr size_t kMaxFilteredBlobs = 50;
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800226 if (filtered_blobs.size() >= kMinFilteredBlobs &&
227 filtered_blobs.size() <= kMaxFilteredBlobs) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800228 constexpr size_t kRansacIterations = 15;
229 for (size_t i = 0; i < kRansacIterations; i++) {
230 // Pick 3 random blobs and see how many fit on their circle
231 const size_t j = std::rand() % filtered_blobs.size();
232 const size_t k = std::rand() % filtered_blobs.size();
233 const size_t l = std::rand() % filtered_blobs.size();
234
235 // Restart if the random indices clash
236 if ((j == k) || (j == l) || (k == l)) {
237 i--;
238 continue;
239 }
240
241 std::vector<std::vector<cv::Point>> current_blobs{
242 filtered_blobs[j], filtered_blobs[k], filtered_blobs[l]};
243 std::vector<cv::Point2d> current_centroids{filtered_stats[j].centroid,
244 filtered_stats[k].centroid,
245 filtered_stats[l].centroid};
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800246 const std::optional<Circle> current_circle =
247 Circle::Fit(current_centroids);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800248
249 // Make sure that a circle could be created from the points
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800250 if (!current_circle) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800251 continue;
252 }
253
Milind Upadhyayec41e132022-02-05 17:14:05 -0800254 // Only try to fit points to this circle if all of these are between
255 // certain angles.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800256 if (current_circle->InAngleRange(current_centroids[0], kMinBlobAngle,
257 kMaxBlobAngle) &&
258 current_circle->InAngleRange(current_centroids[1], kMinBlobAngle,
259 kMaxBlobAngle) &&
260 current_circle->InAngleRange(current_centroids[2], kMinBlobAngle,
261 kMaxBlobAngle)) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800262 for (size_t m = 0; m < filtered_blobs.size(); m++) {
263 // Add this blob to the list if it is close to the circle, is on the
264 // top half, and isn't one of the other blobs
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800265 if ((m != j) && (m != k) && (m != l) &&
266 current_circle->InAngleRange(filtered_stats[m].centroid,
267 kMinBlobAngle, kMaxBlobAngle) &&
268 (current_circle->DistanceTo(filtered_stats[m].centroid) <
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800269 kCircleDistanceThreshold)) {
270 current_blobs.emplace_back(filtered_blobs[m]);
271 current_centroids.emplace_back(filtered_stats[m].centroid);
272 }
273 }
274
275 if (current_blobs.size() > blob_circle.size()) {
276 blob_circle = current_blobs;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800277 circle = *current_circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800278 centroids = current_centroids;
279 }
280 }
281 }
282 }
283
284 cv::Point avg_centroid(-1, -1);
285 if (centroids.size() > 0) {
286 for (auto centroid : centroids) {
287 avg_centroid.x += centroid.x;
288 avg_centroid.y += centroid.y;
289 }
290 avg_centroid.x /= centroids.size();
291 avg_centroid.y /= centroids.size();
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800292
293 for (auto centroid : centroids) {
294 blob_result->filtered_centroids.emplace_back(
295 static_cast<int>(centroid.x), static_cast<int>(centroid.y));
296 }
297
298 // Sort the filtered centroids to make them go from left to right
299 std::sort(blob_result->filtered_centroids.begin(),
300 blob_result->filtered_centroids.end(),
301 [&circle](cv::Point p, cv::Point q) {
302 // If the angle is greater, it is more left and should be
303 // considered "less" for sorting
304 return circle.AngleOf(p) > circle.AngleOf(q);
305 });
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800306 }
307
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800308 blob_result->filtered_blobs = blob_circle;
309 blob_result->centroid = avg_centroid;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800310}
311
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800312void BlobDetector::DrawBlobs(const BlobResult &blob_result,
313 cv::Mat view_image) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800314 CHECK_GT(view_image.cols, 0);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800315 if (blob_result.unfiltered_blobs.size() > 0) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800316 // Draw blobs unfilled, with red color border
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800317 cv::drawContours(view_image, blob_result.unfiltered_blobs, -1,
318 cv::Scalar(0, 0, 255), 0);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800319 }
320
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800321 cv::drawContours(view_image, blob_result.filtered_blobs, -1,
322 cv::Scalar(0, 100, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800323
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800324 static constexpr double kCircleRadius = 2.0;
milind-u92195982022-01-22 20:29:31 -0800325 // Draw blob centroids
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800326 for (auto stats : blob_result.blob_stats) {
327 cv::circle(view_image, stats.centroid, kCircleRadius,
328 cv::Scalar(0, 215, 255), cv::FILLED);
329 }
330 for (auto centroid : blob_result.filtered_centroids) {
331 cv::circle(view_image, centroid, kCircleRadius, cv::Scalar(0, 255, 0),
milind-u61f21e82022-01-23 18:34:11 -0800332 cv::FILLED);
333 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800334
335 // Draw average centroid
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800336 cv::circle(view_image, blob_result.centroid, kCircleRadius,
337 cv::Scalar(255, 255, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800338}
339
Milind Upadhyayec41e132022-02-05 17:14:05 -0800340void BlobDetector::ExtractBlobs(cv::Mat bgr_image,
Milind Upadhyay25610d22022-02-07 15:35:26 -0800341 BlobDetector::BlobResult *blob_result) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800342 auto start = aos::monotonic_clock::now();
Milind Upadhyayec41e132022-02-05 17:14:05 -0800343 blob_result->binarized_image = ThresholdImage(bgr_image);
Milind Upadhyay25610d22022-02-07 15:35:26 -0800344 blob_result->unfiltered_blobs = FindBlobs(blob_result->binarized_image);
345 blob_result->blob_stats = ComputeStats(blob_result->unfiltered_blobs);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800346 FilterBlobs(blob_result);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800347 auto end = aos::monotonic_clock::now();
Jim Ostrowskifec0c332022-02-06 23:28:26 -0800348 VLOG(2) << "Blob detection elapsed time: "
349 << std::chrono::duration<double, std::milli>(end - start).count()
350 << " ms";
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800351}
352
353} // namespace vision
354} // namespace y2022