blob: 4aa453cf2f3d46bece43fe36955715a3bb2ac6a5 [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"
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080010#include "opencv2/highgui/highgui.hpp"
milind-u92195982022-01-22 20:29:31 -080011#include "opencv2/imgproc.hpp"
milind-udb98afa2022-03-01 19:54:57 -080012#include "y2022/vision/geometry.h"
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080013
Milind Upadhyay81711112022-03-13 22:59:19 -070014DEFINE_int32(red_delta, 100,
15 "Required difference between green pixels vs. red");
16DEFINE_int32(blue_delta, -10,
17 "Required difference between green pixels vs. blue");
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080018
19namespace y2022 {
20namespace vision {
21
Milind Upadhyayec41e132022-02-05 17:14:05 -080022cv::Mat BlobDetector::ThresholdImage(cv::Mat bgr_image) {
23 cv::Mat binarized_image(cv::Size(bgr_image.cols, bgr_image.rows), CV_8UC1);
24 for (int row = 0; row < bgr_image.rows; row++) {
25 for (int col = 0; col < bgr_image.cols; col++) {
26 cv::Vec3b pixel = bgr_image.at<cv::Vec3b>(row, col);
Milind Upadhyay81711112022-03-13 22:59:19 -070027 int blue = pixel.val[0];
28 int green = pixel.val[1];
29 int red = pixel.val[2];
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080030 // Simple filter that looks for green pixels sufficiently brigher than
31 // red and blue
Milind Upadhyay81711112022-03-13 22:59:19 -070032 if ((green > blue + FLAGS_blue_delta) &&
33 (green > red + FLAGS_red_delta)) {
milind-u61f21e82022-01-23 18:34:11 -080034 binarized_image.at<uint8_t>(row, col) = 255;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080035 } else {
milind-u61f21e82022-01-23 18:34:11 -080036 binarized_image.at<uint8_t>(row, col) = 0;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080037 }
38 }
39 }
40
Milind Upadhyayae998722022-03-13 12:45:55 -070041 // Fill in the contours on the binarized image so that we don't detect
42 // multiple blobs in one
43 const auto blobs = FindBlobs(binarized_image);
44 for (auto it = blobs.begin(); it < blobs.end(); it++) {
45 cv::drawContours(binarized_image, blobs, it - blobs.begin(),
46 cv::Scalar(255), cv::FILLED);
47 }
48
milind-u61f21e82022-01-23 18:34:11 -080049 return binarized_image;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080050}
51
52std::vector<std::vector<cv::Point>> BlobDetector::FindBlobs(
53 cv::Mat binarized_image) {
54 // find the contours (blob outlines)
55 std::vector<std::vector<cv::Point>> contours;
56 std::vector<cv::Vec4i> hierarchy;
57 cv::findContours(binarized_image, contours, hierarchy, cv::RETR_CCOMP,
58 cv::CHAIN_APPROX_SIMPLE);
59
60 return contours;
61}
62
milind-u61f21e82022-01-23 18:34:11 -080063std::vector<BlobDetector::BlobStats> BlobDetector::ComputeStats(
Milind Upadhyayf61e1482022-02-11 20:42:55 -080064 const std::vector<std::vector<cv::Point>> &blobs) {
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080065 cv::Mat img = cv::Mat::zeros(640, 480, CV_8UC3);
66
milind-u61f21e82022-01-23 18:34:11 -080067 std::vector<BlobDetector::BlobStats> blob_stats;
68 for (auto blob : blobs) {
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080069 // Opencv doesn't have height and width ordered correctly.
70 // The rotated size will only be used after blobs have been filtered, so it
71 // is ok to assume that width is the larger side
72 const cv::Size rotated_rect_size_unordered = cv::minAreaRect(blob).size;
73 const cv::Size rotated_rect_size = {
74 std::max(rotated_rect_size_unordered.width,
75 rotated_rect_size_unordered.height),
76 std::min(rotated_rect_size_unordered.width,
77 rotated_rect_size_unordered.height)};
78 const cv::Size bounding_box_size = cv::boundingRect(blob).size();
79
Milind Upadhyayf61e1482022-02-11 20:42:55 -080080 cv::Moments moments = cv::moments(blob);
milind-u61f21e82022-01-23 18:34:11 -080081
82 const auto centroid =
83 cv::Point(moments.m10 / moments.m00, moments.m01 / moments.m00);
84 const double aspect_ratio =
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080085 static_cast<double>(bounding_box_size.width) / bounding_box_size.height;
milind-u61f21e82022-01-23 18:34:11 -080086 const double area = moments.m00;
Henry Speisere45e7a22022-02-04 23:17:01 -080087 const size_t num_points = blob.size();
milind-u61f21e82022-01-23 18:34:11 -080088
Henry Speisere45e7a22022-02-04 23:17:01 -080089 blob_stats.emplace_back(
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080090 BlobStats{centroid, rotated_rect_size, aspect_ratio, area, num_points});
milind-u61f21e82022-01-23 18:34:11 -080091 }
Milind Upadhyay8f38ad82022-03-03 10:06:18 -080092
milind-u61f21e82022-01-23 18:34:11 -080093 return blob_stats;
94}
95
Milind Upadhyayf61e1482022-02-11 20:42:55 -080096void BlobDetector::FilterBlobs(BlobResult *blob_result) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -080097 std::vector<std::vector<cv::Point>> filtered_blobs;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -080098 std::vector<BlobStats> filtered_stats;
milind-u92195982022-01-22 20:29:31 -080099
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800100 auto blob_it = blob_result->unfiltered_blobs.begin();
101 auto stats_it = blob_result->blob_stats.begin();
102 while (blob_it < blob_result->unfiltered_blobs.end() &&
103 stats_it < blob_result->blob_stats.end()) {
milind-u61f21e82022-01-23 18:34:11 -0800104 constexpr double kTapeAspectRatio = 5.0 / 2.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800105 constexpr double kAspectRatioThreshold = 1.6;
milind-u61f21e82022-01-23 18:34:11 -0800106 constexpr double kMinArea = 10;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800107 constexpr size_t kMinNumPoints = 6;
milind-u92195982022-01-22 20:29:31 -0800108
milind-u61f21e82022-01-23 18:34:11 -0800109 // Remove all blobs that are at the bottom of the image, have a different
Milind Upadhyayec41e132022-02-05 17:14:05 -0800110 // aspect ratio than the tape, or have too little area or points.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800111 if ((std::abs(1.0 - kTapeAspectRatio / stats_it->aspect_ratio) <
milind-u61f21e82022-01-23 18:34:11 -0800112 kAspectRatioThreshold) &&
Milind Upadhyayec41e132022-02-05 17:14:05 -0800113 (stats_it->area >= kMinArea) &&
114 (stats_it->num_points >= kMinNumPoints)) {
milind-u61f21e82022-01-23 18:34:11 -0800115 filtered_blobs.push_back(*blob_it);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800116 filtered_stats.push_back(*stats_it);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800117 }
milind-u61f21e82022-01-23 18:34:11 -0800118 blob_it++;
119 stats_it++;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800120 }
milind-u92195982022-01-22 20:29:31 -0800121
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800122 // Threshold for mean distance from a blob centroid to a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800123 constexpr double kCircleDistanceThreshold = 10.0;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800124 // We should only expect to see blobs between these angles on a circle.
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800125 constexpr double kDegToRad = M_PI / 180.0;
126 constexpr double kMinBlobAngle = 50.0 * kDegToRad;
Milind Upadhyayec41e132022-02-05 17:14:05 -0800127 constexpr double kMaxBlobAngle = M_PI - kMinBlobAngle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800128 std::vector<std::vector<cv::Point>> blob_circle;
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800129 std::vector<BlobStats> blob_circle_stats;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800130 Circle circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800131
132 // If we see more than this number of blobs after filtering based on
133 // color/size, the circle fit may detect noise so just return no blobs.
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800134 constexpr size_t kMinFilteredBlobs = 3;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800135 constexpr size_t kMaxFilteredBlobs = 50;
Milind Upadhyay2b4404c2022-02-04 21:20:57 -0800136 if (filtered_blobs.size() >= kMinFilteredBlobs &&
137 filtered_blobs.size() <= kMaxFilteredBlobs) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800138 constexpr size_t kRansacIterations = 15;
139 for (size_t i = 0; i < kRansacIterations; i++) {
140 // Pick 3 random blobs and see how many fit on their circle
141 const size_t j = std::rand() % filtered_blobs.size();
142 const size_t k = std::rand() % filtered_blobs.size();
143 const size_t l = std::rand() % filtered_blobs.size();
144
145 // Restart if the random indices clash
146 if ((j == k) || (j == l) || (k == l)) {
147 i--;
148 continue;
149 }
150
151 std::vector<std::vector<cv::Point>> current_blobs{
152 filtered_blobs[j], filtered_blobs[k], filtered_blobs[l]};
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800153 std::vector<BlobStats> current_stats{filtered_stats[j], filtered_stats[k],
154 filtered_stats[l]};
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800155 const std::optional<Circle> current_circle =
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800156 Circle::Fit({current_stats[0].centroid, current_stats[1].centroid,
157 current_stats[2].centroid});
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800158
159 // Make sure that a circle could be created from the points
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800160 if (!current_circle) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800161 continue;
162 }
163
Milind Upadhyayec41e132022-02-05 17:14:05 -0800164 // Only try to fit points to this circle if all of these are between
165 // certain angles.
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800166 if (current_circle->InAngleRange(current_stats[0].centroid, kMinBlobAngle,
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800167 kMaxBlobAngle) &&
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800168 current_circle->InAngleRange(current_stats[1].centroid, kMinBlobAngle,
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800169 kMaxBlobAngle) &&
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800170 current_circle->InAngleRange(current_stats[2].centroid, kMinBlobAngle,
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800171 kMaxBlobAngle)) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800172 for (size_t m = 0; m < filtered_blobs.size(); m++) {
173 // Add this blob to the list if it is close to the circle, is on the
174 // top half, and isn't one of the other blobs
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800175 if ((m != j) && (m != k) && (m != l) &&
176 current_circle->InAngleRange(filtered_stats[m].centroid,
177 kMinBlobAngle, kMaxBlobAngle) &&
178 (current_circle->DistanceTo(filtered_stats[m].centroid) <
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800179 kCircleDistanceThreshold)) {
180 current_blobs.emplace_back(filtered_blobs[m]);
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800181 current_stats.emplace_back(filtered_stats[m]);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800182 }
183 }
184
185 if (current_blobs.size() > blob_circle.size()) {
186 blob_circle = current_blobs;
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800187 blob_circle_stats = current_stats;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800188 circle = *current_circle;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800189 }
190 }
191 }
192 }
193
194 cv::Point avg_centroid(-1, -1);
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800195 if (blob_circle.size() > 0) {
196 for (const auto &stats : blob_circle_stats) {
197 avg_centroid.x += stats.centroid.x;
198 avg_centroid.y += stats.centroid.y;
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800199 }
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800200 avg_centroid.x /= blob_circle_stats.size();
201 avg_centroid.y /= blob_circle_stats.size();
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800202 }
203
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800204 blob_result->filtered_blobs = blob_circle;
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800205 blob_result->filtered_stats = blob_circle_stats;
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800206 blob_result->centroid = avg_centroid;
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800207}
208
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800209void BlobDetector::DrawBlobs(const BlobResult &blob_result,
210 cv::Mat view_image) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800211 CHECK_GT(view_image.cols, 0);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800212 if (blob_result.unfiltered_blobs.size() > 0) {
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800213 // Draw blobs unfilled, with red color border
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800214 cv::drawContours(view_image, blob_result.unfiltered_blobs, -1,
215 cv::Scalar(0, 0, 255), 0);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800216 }
217
James Kuszmauld230d7a2022-03-06 15:00:43 -0800218 if (blob_result.filtered_blobs.size() > 0) {
219 cv::drawContours(view_image, blob_result.filtered_blobs, -1,
220 cv::Scalar(0, 100, 0), cv::FILLED);
221 }
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800222
Milind Upadhyay2da80bb2022-03-12 22:54:35 -0800223 for (const auto &blob : blob_result.filtered_blobs) {
224 cv::polylines(view_image, blob, true, cv::Scalar(0, 255, 0));
225 }
226
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800227 static constexpr double kCircleRadius = 2.0;
milind-u92195982022-01-22 20:29:31 -0800228 // Draw blob centroids
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800229 for (auto stats : blob_result.blob_stats) {
230 cv::circle(view_image, stats.centroid, kCircleRadius,
231 cv::Scalar(0, 215, 255), cv::FILLED);
232 }
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800233 for (auto stats : blob_result.filtered_stats) {
234 cv::circle(view_image, stats.centroid, kCircleRadius, cv::Scalar(0, 255, 0),
milind-u61f21e82022-01-23 18:34:11 -0800235 cv::FILLED);
236 }
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800237
238 // Draw average centroid
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800239 cv::circle(view_image, blob_result.centroid, kCircleRadius,
240 cv::Scalar(255, 255, 0), cv::FILLED);
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800241}
242
Milind Upadhyayec41e132022-02-05 17:14:05 -0800243void BlobDetector::ExtractBlobs(cv::Mat bgr_image,
Milind Upadhyay25610d22022-02-07 15:35:26 -0800244 BlobDetector::BlobResult *blob_result) {
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800245 auto start = aos::monotonic_clock::now();
Milind Upadhyayec41e132022-02-05 17:14:05 -0800246 blob_result->binarized_image = ThresholdImage(bgr_image);
Milind Upadhyay25610d22022-02-07 15:35:26 -0800247 blob_result->unfiltered_blobs = FindBlobs(blob_result->binarized_image);
248 blob_result->blob_stats = ComputeStats(blob_result->unfiltered_blobs);
Milind Upadhyayf61e1482022-02-11 20:42:55 -0800249 FilterBlobs(blob_result);
Milind Upadhyaye7aa40c2022-01-29 22:36:21 -0800250 auto end = aos::monotonic_clock::now();
Milind Upadhyay8f38ad82022-03-03 10:06:18 -0800251 VLOG(1) << "Blob detection elapsed time: "
Jim Ostrowskifec0c332022-02-06 23:28:26 -0800252 << std::chrono::duration<double, std::milli>(end - start).count()
253 << " ms";
Jim Ostrowskiff0f5e42022-01-22 01:35:31 -0800254}
255
256} // namespace vision
257} // namespace y2022