blob: f69e9876541f950b56ac84fba8959b9a0e06e131 [file] [log] [blame]
Parker Schuh2a1447c2019-02-17 00:25:29 -08001#include "y2019/vision/target_finder.h"
2
3#include "aos/vision/blob/hierarchical_contour_merge.h"
4
5using namespace aos::vision;
6
7namespace y2019 {
8namespace vision {
9
10TargetFinder::TargetFinder() { target_template_ = Target::MakeTemplate(); }
11
12aos::vision::RangeImage TargetFinder::Threshold(aos::vision::ImagePtr image) {
13 const uint8_t threshold_value = GetThresholdValue();
14 return aos::vision::DoThreshold(image, [&](aos::vision::PixelRef &px) {
15 if (px.g > threshold_value && px.b > threshold_value &&
16 px.r > threshold_value) {
17 return true;
18 }
19 return false;
20 });
21}
22
23// Filter blobs on size.
24void TargetFinder::PreFilter(BlobList *imgs) {
25 imgs->erase(
26 std::remove_if(imgs->begin(), imgs->end(),
27 [](RangeImage &img) {
28 // We can drop images with a small number of
29 // pixels, but images
30 // must be over 20px or the math will have issues.
31 return (img.npixels() < 100 || img.height() < 25);
32 }),
33 imgs->end());
34}
35
36// TODO: Try hierarchical merge for this.
37// Convert blobs into polygons.
38std::vector<aos::vision::Segment<2>> TargetFinder::FillPolygon(
39 const RangeImage &blob, bool verbose) {
40 if (verbose) printf("Process Polygon.\n");
41 alloc_.reset();
42 auto *st = RangeImgToContour(blob, &alloc_);
43
44 struct Pt {
45 float x;
46 float y;
47 };
48 std::vector<Pt> pts;
49
50 // Collect all slopes from the contour.
51 auto opt = st->pt;
52 for (auto *node = st; node->next != st;) {
53 node = node->next;
54
55 auto npt = node->pt;
56
57 pts.push_back(
58 {static_cast<float>(npt.x - opt.x), static_cast<float>(npt.y - opt.y)});
59
60 opt = npt;
61 }
62
63 const int n = pts.size();
64 auto get_pt = [&](int i) { return pts[(i + n * 2) % n]; };
65
66 std::vector<Pt> pts_new = pts;
67 auto run_box_filter = [&](int window_size) {
68 for (size_t i = 0; i < pts.size(); ++i) {
69 Pt a{0.0, 0.0};
70 for (int j = -window_size; j <= window_size; ++j) {
71 Pt p = get_pt(j + i);
72 a.x += p.x;
73 a.y += p.y;
74 }
75 a.x /= (window_size * 2 + 1);
76 a.y /= (window_size * 2 + 1);
77
78 float scale = 1.0 + (i / float(pts.size() * 10));
79 a.x *= scale;
80 a.y *= scale;
81 pts_new[i] = a;
82 }
83 pts = pts_new;
84 };
85 // Three box filter makith a guassian?
86 // Run gaussian filter over the slopes.
87 run_box_filter(2);
88 run_box_filter(2);
89 run_box_filter(2);
90
91 // Heuristic which says if a particular slope is part of a corner.
92 auto is_corner = [&](size_t i) {
93 Pt a = get_pt(i - 3);
94 Pt b = get_pt(i + 3);
95 double dx = (a.x - b.x);
96 double dy = (a.y - b.y);
97 return dx * dx + dy * dy > 0.25;
98 };
99
100 bool prev_v = is_corner(-1);
101
102 // Find all centers of corners.
103 // Because they round, multiple points may be a corner.
104 std::vector<size_t> edges;
105 size_t kBad = pts.size() + 10;
106 size_t prev_up = kBad;
107 size_t wrapped_n = prev_up;
108
109 for (size_t i = 0; i < pts.size(); ++i) {
110 bool v = is_corner(i);
111 if (prev_v && !v) {
112 if (prev_up == kBad) {
113 wrapped_n = i;
114 } else {
115 edges.push_back((prev_up + i - 1) / 2);
116 }
117 }
118 if (v && !prev_v) {
119 prev_up = i;
120 }
121 prev_v = v;
122 }
123
124 if (wrapped_n != kBad) {
125 edges.push_back(((prev_up + pts.size() + wrapped_n - 1) / 2) % pts.size());
126 }
127
128 if (verbose) printf("Edge Count (%zu).\n", edges.size());
129
130 // Get all CountourNodes from the contour.
131 using aos::vision::PixelRef;
132 std::vector<ContourNode *> segments;
133 {
134 std::vector<ContourNode *> segments_all;
135
136 for (ContourNode *node = st; node->next != st;) {
137 node = node->next;
138 segments_all.push_back(node);
139 }
140 for (size_t i : edges) {
141 segments.push_back(segments_all[i]);
142 }
143 }
144 if (verbose) printf("Segment Count (%zu).\n", segments.size());
145
146 // Run best-fits over each line segment.
147 std::vector<Segment<2>> seg_list;
148 if (segments.size() == 4) {
149 for (size_t i = 0; i < segments.size(); ++i) {
150 auto *ed = segments[(i + 1) % segments.size()];
151 auto *st = segments[i];
152 float mx = 0.0;
153 float my = 0.0;
154 int n = 0;
155 for (auto *node = st; node != ed; node = node->next) {
156 mx += node->pt.x;
157 my += node->pt.y;
158 ++n;
159 // (x - [x] / N) ** 2 = [x * x] - 2 * [x] * [x] / N + [x] * [x] / N / N;
160 }
161 mx /= n;
162 my /= n;
163
164 float xx = 0.0;
165 float xy = 0.0;
166 float yy = 0.0;
167 for (auto *node = st; node != ed; node = node->next) {
168 float x = node->pt.x - mx;
169 float y = node->pt.y - my;
170 xx += x * x;
171 xy += x * y;
172 yy += y * y;
173 }
174
175 // TODO: Extract common to hierarchical merge.
176 float neg_b_over_2 = (xx + yy) / 2.0;
177 float c = (xx * yy - xy * xy);
178
179 float sqr = sqrt(neg_b_over_2 * neg_b_over_2 - c);
180
181 {
182 float lam = neg_b_over_2 + sqr;
183 float x = xy;
184 float y = lam - xx;
185
186 float norm = sqrt(x * x + y * y);
187 x /= norm;
188 y /= norm;
189
190 seg_list.push_back(
191 Segment<2>(Vector<2>(mx, my), Vector<2>(mx + x, my + y)));
192 }
193
194 /* Characteristic polynomial
195 1 lam^2 - (xx + yy) lam + (xx * yy - xy * xy) = 0
196
197 [a b]
198 [c d]
199
200 // covariance matrix.
201 [xx xy] [nx]
202 [xy yy] [ny]
203 */
204 }
205 }
206 if (verbose) printf("Poly Count (%zu).\n", seg_list.size());
207 return seg_list;
208}
209
210// Convert segments into target components (left or right)
211std::vector<TargetComponent> TargetFinder::FillTargetComponentList(
212 const std::vector<std::vector<Segment<2>>> &seg_list) {
213 std::vector<TargetComponent> list;
214 TargetComponent new_target;
215 for (const auto &poly : seg_list) {
216 // Reject missized pollygons for now. Maybe rectify them here in the future;
217 if (poly.size() != 4) continue;
218 std::vector<Vector<2>> corners;
219 for (size_t i = 0; i < 4; ++i) {
220 corners.push_back(poly[i].Intersect(poly[(i + 1) % 4]));
221 }
222
223 // Select the closest two points. Short side of the rectangle.
224 double min_dist = -1;
225 std::pair<size_t, size_t> closest;
226 for (size_t i = 0; i < 4; ++i) {
227 size_t next = (i + 1) % 4;
228 double nd = corners[i].SquaredDistanceTo(corners[next]);
229 if (min_dist == -1 || nd < min_dist) {
230 min_dist = nd;
231 closest.first = i;
232 closest.second = next;
233 }
234 }
235
236 // Verify our top is above the bottom.
237 size_t bot_index = closest.first;
238 size_t top_index = (closest.first + 2) % 4;
239 if (corners[top_index].y() < corners[bot_index].y()) {
240 closest.first = top_index;
241 closest.second = (top_index + 1) % 4;
242 }
243
244 // Find the major axis.
245 size_t far_first = (closest.first + 2) % 4;
246 size_t far_second = (closest.second + 2) % 4;
247 Segment<2> major_axis(
248 (corners[closest.first] + corners[closest.second]) * 0.5,
249 (corners[far_first] + corners[far_second]) * 0.5);
250 if (major_axis.AsVector().AngleToZero() > M_PI / 180.0 * 120.0 ||
251 major_axis.AsVector().AngleToZero() < M_PI / 180.0 * 60.0) {
252 // Target is angled way too much, drop it.
253 continue;
254 }
255
256 // organize the top points.
257 Vector<2> topA = corners[closest.first] - major_axis.B();
258 new_target.major_axis = major_axis;
259 if (major_axis.AsVector().AngleToZero() > M_PI / 2.0) {
260 // We have a left target since we are leaning positive.
261 new_target.is_right = false;
262 if (topA.AngleTo(major_axis.AsVector()) > 0.0) {
263 // And our A point is left of the major axis.
264 new_target.inside = corners[closest.second];
265 new_target.top = corners[closest.first];
266 } else {
267 // our A point is to the right of the major axis.
268 new_target.inside = corners[closest.first];
269 new_target.top = corners[closest.second];
270 }
271 } else {
272 // We have a right target since we are leaning negative.
273 new_target.is_right = true;
274 if (topA.AngleTo(major_axis.AsVector()) > 0.0) {
275 // And our A point is left of the major axis.
276 new_target.inside = corners[closest.first];
277 new_target.top = corners[closest.second];
278 } else {
279 // our A point is to the right of the major axis.
280 new_target.inside = corners[closest.second];
281 new_target.top = corners[closest.first];
282 }
283 }
284
285 // organize the top points.
286 Vector<2> botA = corners[far_first] - major_axis.A();
287 if (major_axis.AsVector().AngleToZero() > M_PI / 2.0) {
288 // We have a right target since we are leaning positive.
289 if (botA.AngleTo(major_axis.AsVector()) < M_PI) {
290 // And our A point is left of the major axis.
291 new_target.outside = corners[far_second];
292 new_target.bottom = corners[far_first];
293 } else {
294 // our A point is to the right of the major axis.
295 new_target.outside = corners[far_first];
296 new_target.bottom = corners[far_second];
297 }
298 } else {
299 // We have a left target since we are leaning negative.
300 if (botA.AngleTo(major_axis.AsVector()) < M_PI) {
301 // And our A point is left of the major axis.
302 new_target.outside = corners[far_first];
303 new_target.bottom = corners[far_second];
304 } else {
305 // our A point is to the right of the major axis.
306 new_target.outside = corners[far_second];
307 new_target.bottom = corners[far_first];
308 }
309 }
310
311 // This piece of the target should be ready now.
312 list.emplace_back(new_target);
313 }
314
315 return list;
316}
317
318// Match components into targets.
319std::vector<Target> TargetFinder::FindTargetsFromComponents(
320 const std::vector<TargetComponent> component_list, bool verbose) {
321 std::vector<Target> target_list;
322 using namespace aos::vision;
323 if (component_list.size() < 2) {
324 // We don't enough parts for a target.
325 return target_list;
326 }
327
328 for (size_t i = 0; i < component_list.size(); i++) {
329 const TargetComponent &a = component_list[i];
330 for (size_t j = 0; j < i; j++) {
331 bool target_valid = false;
332 Target new_target;
333 const TargetComponent &b = component_list[j];
334
335 // Reject targets that are too far off vertically.
336 Vector<2> a_center = a.major_axis.Center();
337 if (a_center.y() > b.bottom.y() || a_center.y() < b.top.y()) {
338 continue;
339 }
340 Vector<2> b_center = b.major_axis.Center();
341 if (b_center.y() > a.bottom.y() || b_center.y() < a.top.y()) {
342 continue;
343 }
344
345 if (a.is_right && !b.is_right) {
346 if (a.top.x() > b.top.x()) {
347 new_target.right = a;
348 new_target.left = b;
349 target_valid = true;
350 }
351 } else if (!a.is_right && b.is_right) {
352 if (b.top.x() > a.top.x()) {
353 new_target.right = b;
354 new_target.left = a;
355 target_valid = true;
356 }
357 }
358 if (target_valid) {
359 target_list.emplace_back(new_target);
360 }
361 }
362 }
363 if (verbose) printf("Possible Target: %zu.\n", target_list.size());
364 return target_list;
365}
366
367std::vector<IntermediateResult> TargetFinder::FilterResults(
368 const std::vector<IntermediateResult> &results) {
369 std::vector<IntermediateResult> filtered;
370 for (const IntermediateResult &res : results) {
371 if (res.solver_error < 75.0) {
372 filtered.emplace_back(res);
373 }
374 }
375 return filtered;
376}
377
378} // namespace vision
379} // namespace y2019