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