Parker Schuh | f7481be | 2017-03-04 18:24:33 -0800 | [diff] [blame] | 1 | #include "y2017/vision/target_finder.h" |
| 2 | |
| 3 | #include <math.h> |
| 4 | |
| 5 | namespace y2017 { |
| 6 | namespace 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. |
| 12 | void 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 | |
| 66 | double 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 | |
| 83 | std::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 | |
| 105 | aos::vision::RangeImage TargetFinder::Threshold(aos::vision::ImagePtr image) { |
| 106 | return aos::vision::DoThreshold(image, [&](aos::vision::PixelRef &px) { |
| 107 | if (px.g > 88) { |
Parker Schuh | f7481be | 2017-03-04 18:24:33 -0800 | [diff] [blame] | 108 | 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 | |
| 119 | void 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 | |
| 130 | bool 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. |
Austin Schuh | 218a755 | 2017-03-22 21:15:28 -0700 | [diff] [blame^] | 160 | double regressed_y_center = 0; |
Parker Schuh | f7481be | 2017-03-04 18:24:33 -0800 | [diff] [blame] | 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 Schuh | abb6b6c | 2017-03-11 16:31:24 -0800 | [diff] [blame] | 236 | namespace { |
| 237 | |
| 238 | constexpr double kInchesToMeters = 0.0254; |
| 239 | |
| 240 | } // namespace |
| 241 | |
| 242 | void 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 | |
| 250 | void 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); |
Austin Schuh | 218a755 | 2017-03-22 21:15:28 -0700 | [diff] [blame^] | 277 | *angle = -std::atan2(px, pz); |
Parker Schuh | abb6b6c | 2017-03-11 16:31:24 -0800 | [diff] [blame] | 278 | } |
| 279 | |
Parker Schuh | f7481be | 2017-03-04 18:24:33 -0800 | [diff] [blame] | 280 | } // namespace vision |
| 281 | } // namespace y2017 |