blob: 68832fd96a66426d685de5ce52aa7bf2747a9ca5 [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"
milind-udb98afa2022-03-01 19:54:57 -080011#include "y2022/vision/geometry.h"
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080012
Yash Chainani6acad6f2022-02-03 10:52:53 -080013DEFINE_uint64(red_delta, 100,
14 "Required difference between green pixels vs. red");
15DEFINE_uint64(blue_delta, 50,
16 "Required difference between green pixels vs. blue");
17
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080018DEFINE_bool(use_outdoors, false,
19 "If true, change thresholds to handle outdoor illumination");
Yash Chainani6acad6f2022-02-03 10:52:53 -080020DEFINE_uint64(outdoors_red_delta, 100,
21 "Difference between green pixels vs. red, when outdoors");
Milind Upadhyayf61e1482022-02-11 20:42:55 -080022DEFINE_uint64(outdoors_blue_delta, 1,
Yash Chainani6acad6f2022-02-03 10:52:53 -080023 "Difference between green pixels vs. blue, when outdoors");
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080024
25namespace y2022 {
26namespace vision {
27
Milind Upadhyayec41e132022-02-05 17:14:05 -080028cv::Mat BlobDetector::ThresholdImage(cv::Mat bgr_image) {
Yash Chainani6acad6f2022-02-03 10:52:53 -080029 size_t red_delta = FLAGS_red_delta;
30 size_t blue_delta = FLAGS_blue_delta;
31
32 if (FLAGS_use_outdoors) {
33 red_delta = FLAGS_outdoors_red_delta;
Milind Upadhyayf61e1482022-02-11 20:42:55 -080034 blue_delta = FLAGS_outdoors_blue_delta;
Yash Chainani6acad6f2022-02-03 10:52:53 -080035 }
36
Milind Upadhyayec41e132022-02-05 17:14:05 -080037 cv::Mat binarized_image(cv::Size(bgr_image.cols, bgr_image.rows), CV_8UC1);
38 for (int row = 0; row < bgr_image.rows; row++) {
39 for (int col = 0; col < bgr_image.cols; col++) {
40 cv::Vec3b pixel = bgr_image.at<cv::Vec3b>(row, col);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080041 uint8_t blue = pixel.val[0];
42 uint8_t green = pixel.val[1];
43 uint8_t red = pixel.val[2];
44 // Simple filter that looks for green pixels sufficiently brigher than
45 // red and blue
Yash Chainani6acad6f2022-02-03 10:52:53 -080046 if ((green > blue + blue_delta) && (green > red + red_delta)) {
milind-u61f21e82022-01-23 18:34:11 -080047 binarized_image.at<uint8_t>(row, col) = 255;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080048 } else {
milind-u61f21e82022-01-23 18:34:11 -080049 binarized_image.at<uint8_t>(row, col) = 0;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080050 }
51 }
52 }
53
milind-u61f21e82022-01-23 18:34:11 -080054 return binarized_image;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080055}
56
57std::vector<std::vector<cv::Point>> BlobDetector::FindBlobs(
58 cv::Mat binarized_image) {
59 // find the contours (blob outlines)
60 std::vector<std::vector<cv::Point>> contours;
61 std::vector<cv::Vec4i> hierarchy;
62 cv::findContours(binarized_image, contours, hierarchy, cv::RETR_CCOMP,
63 cv::CHAIN_APPROX_SIMPLE);
64
65 return contours;
66}
67
milind-u61f21e82022-01-23 18:34:11 -080068std::vector<BlobDetector::BlobStats> BlobDetector::ComputeStats(
Milind Upadhyayf61e1482022-02-11 20:42:55 -080069 const std::vector<std::vector<cv::Point>> &blobs) {
milind-u61f21e82022-01-23 18:34:11 -080070 std::vector<BlobDetector::BlobStats> blob_stats;
71 for (auto blob : blobs) {
Milind Upadhyayf61e1482022-02-11 20:42:55 -080072 auto blob_size = cv::boundingRect(blob).size();
73 cv::Moments moments = cv::moments(blob);
milind-u61f21e82022-01-23 18:34:11 -080074
75 const auto centroid =
76 cv::Point(moments.m10 / moments.m00, moments.m01 / moments.m00);
77 const double aspect_ratio =
78 static_cast<double>(blob_size.width) / blob_size.height;
79 const double area = moments.m00;
Henry Speisere45e7a22022-02-04 23:17:01 -080080 const size_t num_points = blob.size();
milind-u61f21e82022-01-23 18:34:11 -080081
Henry Speisere45e7a22022-02-04 23:17:01 -080082 blob_stats.emplace_back(
83 BlobStats{centroid, aspect_ratio, area, num_points});
milind-u61f21e82022-01-23 18:34:11 -080084 }
85 return blob_stats;
86}
87
Milind Upadhyayf61e1482022-02-11 20:42:55 -080088void BlobDetector::FilterBlobs(BlobResult *blob_result) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080089 std::vector<std::vector<cv::Point>> filtered_blobs;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -080090 std::vector<BlobStats> filtered_stats;
milind-u92195982022-01-22 20:29:31 -080091
Milind Upadhyayf61e1482022-02-11 20:42:55 -080092 auto blob_it = blob_result->unfiltered_blobs.begin();
93 auto stats_it = blob_result->blob_stats.begin();
94 while (blob_it < blob_result->unfiltered_blobs.end() &&
95 stats_it < blob_result->blob_stats.end()) {
milind-u61f21e82022-01-23 18:34:11 -080096 constexpr double kTapeAspectRatio = 5.0 / 2.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -080097 constexpr double kAspectRatioThreshold = 1.6;
milind-u61f21e82022-01-23 18:34:11 -080098 constexpr double kMinArea = 10;
Milind Upadhyayec41e132022-02-05 17:14:05 -080099 constexpr size_t kMinNumPoints = 6;
milind-u92195982022-01-22 20:29:31 -0800100
milind-u61f21e82022-01-23 18:34:11 -0800101 // Remove all blobs that are at the bottom of the image, have a different
Milind Upadhyayec41e132022-02-05 17:14:05 -0800102 // aspect ratio than the tape, or have too little area or points.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800103 if ((std::abs(1.0 - kTapeAspectRatio / stats_it->aspect_ratio) <
milind-u61f21e82022-01-23 18:34:11 -0800104 kAspectRatioThreshold) &&
Milind Upadhyayec41e132022-02-05 17:14:05 -0800105 (stats_it->area >= kMinArea) &&
106 (stats_it->num_points >= kMinNumPoints)) {
milind-u61f21e82022-01-23 18:34:11 -0800107 filtered_blobs.push_back(*blob_it);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800108 filtered_stats.push_back(*stats_it);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800109 }
milind-u61f21e82022-01-23 18:34:11 -0800110 blob_it++;
111 stats_it++;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800112 }
milind-u92195982022-01-22 20:29:31 -0800113
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800114 // Threshold for mean distance from a blob centroid to a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800115 constexpr double kCircleDistanceThreshold = 10.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800116 // We should only expect to see blobs between these angles on a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800117 constexpr double kDegToRad = M_PI / 180.0;
118 constexpr double kMinBlobAngle = 50.0 * kDegToRad;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800119 constexpr double kMaxBlobAngle = M_PI - kMinBlobAngle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800120 std::vector<std::vector<cv::Point>> blob_circle;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800121 Circle circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800122 std::vector<cv::Point2d> centroids;
123
124 // If we see more than this number of blobs after filtering based on
125 // color/size, the circle fit may detect noise so just return no blobs.
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800126 constexpr size_t kMinFilteredBlobs = 3;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800127 constexpr size_t kMaxFilteredBlobs = 50;
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800128 if (filtered_blobs.size() >= kMinFilteredBlobs &&
129 filtered_blobs.size() <= kMaxFilteredBlobs) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800130 constexpr size_t kRansacIterations = 15;
131 for (size_t i = 0; i < kRansacIterations; i++) {
132 // Pick 3 random blobs and see how many fit on their circle
133 const size_t j = std::rand() % filtered_blobs.size();
134 const size_t k = std::rand() % filtered_blobs.size();
135 const size_t l = std::rand() % filtered_blobs.size();
136
137 // Restart if the random indices clash
138 if ((j == k) || (j == l) || (k == l)) {
139 i--;
140 continue;
141 }
142
143 std::vector<std::vector<cv::Point>> current_blobs{
144 filtered_blobs[j], filtered_blobs[k], filtered_blobs[l]};
145 std::vector<cv::Point2d> current_centroids{filtered_stats[j].centroid,
146 filtered_stats[k].centroid,
147 filtered_stats[l].centroid};
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800148 const std::optional<Circle> current_circle =
149 Circle::Fit(current_centroids);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800150
151 // Make sure that a circle could be created from the points
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800152 if (!current_circle) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800153 continue;
154 }
155
Milind Upadhyayec41e132022-02-05 17:14:05 -0800156 // Only try to fit points to this circle if all of these are between
157 // certain angles.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800158 if (current_circle->InAngleRange(current_centroids[0], kMinBlobAngle,
159 kMaxBlobAngle) &&
160 current_circle->InAngleRange(current_centroids[1], kMinBlobAngle,
161 kMaxBlobAngle) &&
162 current_circle->InAngleRange(current_centroids[2], kMinBlobAngle,
163 kMaxBlobAngle)) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800164 for (size_t m = 0; m < filtered_blobs.size(); m++) {
165 // Add this blob to the list if it is close to the circle, is on the
166 // top half, and isn't one of the other blobs
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800167 if ((m != j) && (m != k) && (m != l) &&
168 current_circle->InAngleRange(filtered_stats[m].centroid,
169 kMinBlobAngle, kMaxBlobAngle) &&
170 (current_circle->DistanceTo(filtered_stats[m].centroid) <
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800171 kCircleDistanceThreshold)) {
172 current_blobs.emplace_back(filtered_blobs[m]);
173 current_centroids.emplace_back(filtered_stats[m].centroid);
174 }
175 }
176
177 if (current_blobs.size() > blob_circle.size()) {
178 blob_circle = current_blobs;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800179 circle = *current_circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800180 centroids = current_centroids;
181 }
182 }
183 }
184 }
185
186 cv::Point avg_centroid(-1, -1);
187 if (centroids.size() > 0) {
188 for (auto centroid : centroids) {
189 avg_centroid.x += centroid.x;
190 avg_centroid.y += centroid.y;
191 }
192 avg_centroid.x /= centroids.size();
193 avg_centroid.y /= centroids.size();
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800194
195 for (auto centroid : centroids) {
196 blob_result->filtered_centroids.emplace_back(
197 static_cast<int>(centroid.x), static_cast<int>(centroid.y));
198 }
199
200 // Sort the filtered centroids to make them go from left to right
201 std::sort(blob_result->filtered_centroids.begin(),
202 blob_result->filtered_centroids.end(),
203 [&circle](cv::Point p, cv::Point q) {
204 // If the angle is greater, it is more left and should be
205 // considered "less" for sorting
206 return circle.AngleOf(p) > circle.AngleOf(q);
207 });
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800208 }
209
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800210 blob_result->filtered_blobs = blob_circle;
211 blob_result->centroid = avg_centroid;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800212}
213
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800214void BlobDetector::DrawBlobs(const BlobResult &blob_result,
215 cv::Mat view_image) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800216 CHECK_GT(view_image.cols, 0);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800217 if (blob_result.unfiltered_blobs.size() > 0) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800218 // Draw blobs unfilled, with red color border
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800219 cv::drawContours(view_image, blob_result.unfiltered_blobs, -1,
220 cv::Scalar(0, 0, 255), 0);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800221 }
222
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800223 cv::drawContours(view_image, blob_result.filtered_blobs, -1,
224 cv::Scalar(0, 100, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800225
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800226 static constexpr double kCircleRadius = 2.0;
milind-u92195982022-01-22 20:29:31 -0800227 // Draw blob centroids
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800228 for (auto stats : blob_result.blob_stats) {
229 cv::circle(view_image, stats.centroid, kCircleRadius,
230 cv::Scalar(0, 215, 255), cv::FILLED);
231 }
232 for (auto centroid : blob_result.filtered_centroids) {
233 cv::circle(view_image, centroid, kCircleRadius, cv::Scalar(0, 255, 0),
milind-u61f21e82022-01-23 18:34:11 -0800234 cv::FILLED);
235 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800236
237 // Draw average centroid
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800238 cv::circle(view_image, blob_result.centroid, kCircleRadius,
239 cv::Scalar(255, 255, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800240}
241
Milind Upadhyayec41e132022-02-05 17:14:05 -0800242void BlobDetector::ExtractBlobs(cv::Mat bgr_image,
Milind Upadhyay25610d22022-02-07 15:35:26 -0800243 BlobDetector::BlobResult *blob_result) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800244 auto start = aos::monotonic_clock::now();
Milind Upadhyayec41e132022-02-05 17:14:05 -0800245 blob_result->binarized_image = ThresholdImage(bgr_image);
Milind Upadhyay25610d22022-02-07 15:35:26 -0800246 blob_result->unfiltered_blobs = FindBlobs(blob_result->binarized_image);
247 blob_result->blob_stats = ComputeStats(blob_result->unfiltered_blobs);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800248 FilterBlobs(blob_result);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800249 auto end = aos::monotonic_clock::now();
Jim Ostrowskifec0c332022-02-06 23:28:26 -0800250 VLOG(2) << "Blob detection elapsed time: "
251 << std::chrono::duration<double, std::milli>(end - start).count()
252 << " ms";
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800253}
254
255} // namespace vision
256} // namespace y2022