blob: 640c7fe606cd9ea01dddb42635a20e523f3163c6 [file] [log] [blame]
Parker Schuhf7481be2017-03-04 18:24:33 -08001#include "y2017/vision/target_finder.h"
2
Tyler Chatowbf0609c2021-07-31 16:13:27 -07003#include <cmath>
Parker Schuhf7481be2017-03-04 18:24:33 -08004
Stephan Pleinesf63bde82024-01-13 15:59:33 -08005namespace y2017::vision {
Parker Schuhf7481be2017-03-04 18:24:33 -08006
7// Blobs now come in three types:
8// 0) normal blob.
9// 1) first part of a split blob.
10// 2) second part of a split blob.
11void ComputeXShiftPolynomial(int type, const RangeImage &img,
12 TargetComponent *target) {
13 target->img = &img;
14 RangeImage t_img = Transpose(img);
15 int spacing = 10;
16 int n = t_img.size() - spacing * 2;
17 target->n = n;
18 if (n <= 0) {
19 printf("Empty blob aborting (%d).\n", n);
20 return;
21 }
22 Eigen::MatrixXf A = Eigen::MatrixXf::Zero(n * 2, 4);
23 Eigen::VectorXf b = Eigen::VectorXf::Zero(n * 2);
24 int i = 0;
25 for (const auto &row : t_img) {
26 // We decided this was a split target, but this is not a split row.
27 if (i >= spacing && i - spacing < n) {
28 int j = (i - spacing) * 2;
29 // normal blob or the first part of a split.
30 if (type == 0 || type == 1) {
31 b(j) = row[0].st;
32 } else {
33 b(j) = row[1].st;
34 }
35 A(j, 0) = (i) * (i);
36 A(j, 1) = (i);
37 A(j, 2) = 1;
38 ++j;
39 // normal target or the second part of a split.
40 if (type == 0 || type == 2) {
41 b(j) = row[row.size() - 1].ed;
42 } else {
43 b(j) = row[0].ed;
44 }
45 A(j, 0) = i * i;
46 A(j, 1) = i;
47 A(j, 3) = 1;
48 }
49 ++i;
50 }
51 Eigen::VectorXf sol =
52 A.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b);
53 target->a = sol(0);
54 target->b = sol(1);
55 target->c_0 = sol(2);
56 target->c_1 = sol(3);
57
58 target->mini = t_img.min_y();
59
60 Eigen::VectorXf base = A * sol;
61 Eigen::VectorXf error_v = b - base;
62 target->fit_error = error_v.dot(error_v);
63}
64
65double TargetFinder::DetectConnectedTarget(const RangeImage &img) {
66 using namespace aos::vision;
67 RangeImage t_img = Transpose(img);
68 int total = 0;
69 int split = 0;
Parker Schuhf7481be2017-03-04 18:24:33 -080070 for (const auto &row : t_img) {
71 if (row.size() == 1) {
72 total++;
73 } else if (row.size() == 2) {
74 split++;
75 }
Parker Schuhf7481be2017-03-04 18:24:33 -080076 }
77 return (double)split / total;
78}
79
80std::vector<TargetComponent> TargetFinder::FillTargetComponentList(
81 const BlobList &blobs) {
82 std::vector<TargetComponent> list;
83 TargetComponent newTarg;
84 for (std::size_t i = 0; i < blobs.size(); ++i) {
85 double split_ratio;
86 if ((split_ratio = DetectConnectedTarget(blobs[i])) > 0.50) {
87 // Split type blob, do it two parts.
88 ComputeXShiftPolynomial(1, blobs[i], &newTarg);
89 list.emplace_back(newTarg);
90 ComputeXShiftPolynomial(2, blobs[i], &newTarg);
91 list.emplace_back(newTarg);
92 } else {
93 // normal type blob.
94 ComputeXShiftPolynomial(0, blobs[i], &newTarg);
95 list.emplace_back(newTarg);
96 }
97 }
98
99 return list;
100}
101
102aos::vision::RangeImage TargetFinder::Threshold(aos::vision::ImagePtr image) {
Brian Silverman37b15b32019-03-10 13:30:18 -0700103 return aos::vision::ThresholdImageWithFunction(
104 image, [&](aos::vision::PixelRef px) {
105 if (px.g > 88) {
106 uint8_t min = std::min(px.b, px.r);
107 uint8_t max = std::max(px.b, px.r);
108 if (min >= px.g || max >= px.g) return false;
109 uint8_t a = px.g - min;
110 uint8_t b = px.g - max;
111 return (a > 10 && b > 10);
112 }
113 return false;
114 });
Parker Schuhf7481be2017-03-04 18:24:33 -0800115}
116
117void TargetFinder::PreFilter(BlobList &imgs) {
118 imgs.erase(std::remove_if(imgs.begin(), imgs.end(),
119 [](RangeImage &img) {
120 // We can drop images with a small number of
121 // pixels, but images
122 // must be over 20px or the math will have issues.
123 return (img.npixels() < 100 || img.height() < 25);
124 }),
125 imgs.end());
126}
127
128bool TargetFinder::FindTargetFromComponents(
129 std::vector<TargetComponent> component_list, Target *final_target) {
130 using namespace aos::vision;
131 if (component_list.size() < 2 || final_target == NULL) {
132 // We don't enough parts for a traget.
133 return false;
134 }
135
136 // A0 * c + A1*s = b
137 Eigen::MatrixXf A = Eigen::MatrixXf::Zero(4, 2);
138 // A0: Offset component will be constant across all equations.
139 A(0, 0) = 1;
140 A(1, 0) = 1;
141 A(2, 0) = 1;
142 A(3, 0) = 1;
143
144 // A1: s defines the scaling and defines an expexted target.
145 // So these are the center heights of the top and bottom of the two targets.
146 A(0, 1) = -1;
147 A(1, 1) = 0;
148 A(2, 1) = 2;
149 A(3, 1) = 4;
150
151 // Track which pair is the best fit.
152 double best_error = -1;
153 double best_offset = -1;
154 Eigen::VectorXf best_v;
155 // Write down the two indicies.
156 std::pair<int, int> selected;
157 // We are regressing the combined estimated center, might write that down.
Austin Schuh218a7552017-03-22 21:15:28 -0700158 double regressed_y_center = 0;
Parker Schuhf7481be2017-03-04 18:24:33 -0800159
160 Eigen::VectorXf b = Eigen::VectorXf::Zero(4);
161 for (size_t i = 0; i < component_list.size(); i++) {
162 for (size_t j = 0; j < component_list.size(); j++) {
163 if (i == j) {
164 continue;
165 } else {
166 if (component_list[i].a < 0.0 || component_list[j].a < 0.0) {
167 // one of the targets is upside down (ie curved up), this can't
168 // happen.
169 continue;
170 }
171 // b is the target offests.
172 b(0) = component_list[j].EvalMinTop();
173 b(1) = component_list[j].EvalMinBot();
174 b(2) = component_list[i].EvalMinTop();
175 b(3) = component_list[i].EvalMinBot();
176 }
177
178 Eigen::VectorXf sol =
179 A.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b);
180
181 Eigen::VectorXf base = A * sol;
182 Eigen::VectorXf error_v = b - base;
183 double error = error_v.dot(error_v);
184 // offset in scrren x of the two targets.
185 double offset = std::abs(component_list[i].CenterPolyOne() -
186 component_list[j].CenterPolyOne());
187 // How much do we care about offset. As far as I've seen, error is like
188 // 5-20, offset are around 10. Value selected for worst garbage can image.
189 const double offsetWeight = 2.1;
190 error += offsetWeight * offset;
Tyler Chatow63b91ec2017-10-15 13:19:21 -0700191 if ((best_error < 0 || error < best_error) && !::std::isnan(error)) {
Parker Schuhf7481be2017-03-04 18:24:33 -0800192 best_error = error;
193 best_offset = offset;
194 best_v = error_v;
195 selected.first = i;
196 selected.second = j;
197 regressed_y_center = sol(0);
198 }
199 }
200 }
201
202 // If we missed or the error is ridiculous just give up here.
Tyler Chatow63b91ec2017-10-15 13:19:21 -0700203 if (best_error < 0 || best_error > 300.0 || ::std::isnan(best_error)) {
Parker Schuhf7481be2017-03-04 18:24:33 -0800204 fprintf(stderr, "Bogus target dude (%f).\n", best_error);
205 return false;
206 }
207
208 fprintf(stderr,
209 "Selected (%d, %d):\n\t"
210 "err(%.2f, %.2f, %.2f, %.2f)(%.2f)(%.2f).\n\t"
211 "c00(%.2f, %.2f)(%.2f)\n",
212 selected.first, selected.second, best_v(0), best_v(1), best_v(2),
213 best_v(3), best_error, best_offset,
214 component_list[selected.first].CenterPolyOne(),
215 component_list[selected.second].CenterPolyOne(),
216 component_list[selected.first].CenterPolyOne() -
217 component_list[selected.second].CenterPolyOne());
218
219 double avgOff = (component_list[selected.first].mini +
220 component_list[selected.second].mini) /
221 2.0;
222 double avgOne = (component_list[selected.first].CenterPolyOne() +
223 component_list[selected.second].CenterPolyOne()) /
224 2.0;
225
226 final_target->screen_coord.x(avgOne + avgOff);
227 final_target->screen_coord.y(regressed_y_center);
228 final_target->comp1 = component_list[selected.first];
229 final_target->comp2 = component_list[selected.second];
230
231 return true;
232}
233
Parker Schuhabb6b6c2017-03-11 16:31:24 -0800234namespace {
235
236constexpr double kInchesToMeters = 0.0254;
237
238} // namespace
239
240void RotateAngle(aos::vision::Vector<2> vec, double angle, double *rx,
241 double *ry) {
242 double cos_ang = std::cos(angle);
243 double sin_ang = std::sin(angle);
244 *rx = vec.x() * cos_ang - vec.y() * sin_ang;
245 *ry = vec.x() * sin_ang + vec.y() * cos_ang;
246}
247
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700248void TargetFinder::GetAngleDist(const aos::vision::Vector<2> &target,
Parker Schuhabb6b6c2017-03-11 16:31:24 -0800249 double down_angle, double *dist,
250 double *angle) {
251 // TODO(ben): Will put all these numbers in a config file before
252 // the first competition. I hope.
253 double focal_length = 1418.6;
254 double mounted_angle_deg = 33.5;
255 double camera_angle = mounted_angle_deg * M_PI / 180.0 - down_angle;
256 double window_height = 960.0;
257 double window_width = 1280.0;
258
259 double target_height = 78.0;
260 double camera_height = 21.5;
261 double tape_width = 2;
262 double world_height = tape_width + target_height - camera_height;
263
264 double target_to_boiler = 9.5;
265 double robot_to_camera = 9.5;
266 double added_dist = target_to_boiler + robot_to_camera;
267
268 double px = target.x() - window_width / 2.0;
269 double py = target.y() - window_height / 2.0;
270 double pz = focal_length;
271 RotateAngle(aos::vision::Vector<2>(pz, -py), camera_angle, &pz, &py);
272 double pl = std::sqrt(pz * pz + px * px);
273
274 *dist = kInchesToMeters * (world_height * pl / py - added_dist);
Austin Schuh218a7552017-03-22 21:15:28 -0700275 *angle = -std::atan2(px, pz);
Parker Schuhabb6b6c2017-03-11 16:31:24 -0800276}
277
Stephan Pleinesf63bde82024-01-13 15:59:33 -0800278} // namespace y2017::vision