#include "light_bar.hpp" #include #include #include namespace rm27 { // ------------------------------------------------------------------ // 配置加载 // ------------------------------------------------------------------ bool LightBarDetector::loadConfig(const std::string &yaml_path) { cv::FileStorage fs(yaml_path, cv::FileStorage::READ); if (!fs.isOpened()) { std::fprintf(stderr, "[LightBar] 打不开 %s,使用内置默认参数(敌方=蓝)\n", yaml_path.c_str()); return false; } auto readI = [&](const char *k, int &v) { if (!fs[k].isNone()) fs[k] >> v; }; auto readD = [&](const char *k, double &v) { if (!fs[k].isNone()) fs[k] >> v; }; auto readF = [&](const char *k, float &v) { if (!fs[k].isNone()) fs[k] >> v; }; int tmp = 0; fs["enemy_color"] >> cfg_.enemy_color; readI("blue_h_min", cfg_.blue_h_min); readI("blue_h_max", cfg_.blue_h_max); readI("blue_s_min", cfg_.blue_s_min); readI("blue_v_min", cfg_.blue_v_min); readI("red_h_min_1", cfg_.red_h_min_1); readI("red_h_max_1", cfg_.red_h_max_1); readI("red_h_min_2", cfg_.red_h_min_2); readI("red_h_max_2", cfg_.red_h_max_2); readI("red_s_min", cfg_.red_s_min); readI("red_v_min", cfg_.red_v_min); readI("morph_kernel", cfg_.morph_kernel); readI("morph_iters", cfg_.morph_iters); readD("min_length", cfg_.min_length); readD("min_ratio", cfg_.min_ratio); readD("max_tilt_deg", cfg_.max_tilt_deg); readD("max_area", cfg_.max_area); readD("max_angle_diff_deg", cfg_.max_angle_diff_deg); readD("max_len_ratio", cfg_.max_len_ratio); readD("max_center_y_ratio", cfg_.max_center_y_ratio); readD("big_small_ratio_thres", cfg_.big_small_ratio_thres); if (!fs["use_pca"].isNone()) { fs["use_pca"] >> tmp; cfg_.use_pca = tmp != 0; } readI("pass_optimize_lightbar_width", cfg_.pass_optimize_lightbar_width); readF("normalize_max_brightness", cfg_.normalize_max_brightness); readF("lightbar_min_mean_brightness", cfg_.lightbar_min_mean_brightness); readF("padding_scale", cfg_.padding_scale); readF("search_start_ratio", cfg_.search_start_ratio); readF("search_end_ratio", cfg_.search_end_ratio); fs.release(); std::printf("[LightBar] %s 已加载: 敌方=%s PCA=%s\n", yaml_path.c_str(), cfg_.enemy_color.c_str(), cfg_.use_pca ? "开" : "关"); return true; } // ------------------------------------------------------------------ // 轮廓 → 灯条(借鉴 jlu types.cpp LightBar(contour)) // ------------------------------------------------------------------ LightBar LightBarDetector::fromContour(const std::vector &contour) { LightBar bar; bar.rect = cv::minAreaRect(contour); // 用轮廓质心当中心(比矩形对角线交点稳) cv::Point2f c(0.f, 0.f); for (const auto &p : contour) { c += cv::Point2f(static_cast(p.x), static_cast(p.y)); } bar.center = c / static_cast(contour.size()); // 4 角按 y 排序 → 上两点中点=top,下两点中点=bottom cv::Point2f p[4]; bar.rect.points(p); std::sort(p, p + 4, [](const cv::Point2f &a, const cv::Point2f &b) { return a.y < b.y; }); bar.top = (p[0] + p[1]) * 0.5f; bar.bottom = (p[2] + p[3]) * 0.5f; bar.length = cv::norm(bar.top - bar.bottom); bar.width = cv::norm(p[0] - p[1]); cv::Point2f ax = bar.top - bar.bottom; const float n = cv::norm(ax); bar.axis = n > 0.f ? ax / n : cv::Point2f(0.f, -1.f); bar.tilt_deg = static_cast( std::atan2(std::fabs(bar.axis.x), std::fabs(bar.axis.y)) * 180.0 / CV_PI); bar.signed_tilt = std::atan2(static_cast(bar.axis.x), -static_cast(bar.axis.y)); return bar; } // ------------------------------------------------------------------ // 颜色掩膜 → 灯条候选 // ------------------------------------------------------------------ std::vector LightBarDetector::findBars(const cv::Mat &bgr) { cv::Mat hsv; cv::cvtColor(bgr, hsv, cv::COLOR_BGR2HSV); cv::Mat mask(hsv.size(), CV_8UC1, cv::Scalar(0)); const bool blue = (cfg_.enemy_color == "blue" || cfg_.enemy_color == "Blue"); if (blue) { cv::inRange(hsv, cv::Scalar(cfg_.blue_h_min, cfg_.blue_s_min, cfg_.blue_v_min), cv::Scalar(cfg_.blue_h_max, 255, 255), mask); } else { cv::Mat m1, m2; cv::inRange(hsv, cv::Scalar(cfg_.red_h_min_1, cfg_.red_s_min, cfg_.red_v_min), cv::Scalar(cfg_.red_h_max_1, 255, 255), m1); cv::inRange(hsv, cv::Scalar(cfg_.red_h_min_2, cfg_.red_s_min, cfg_.red_v_min), cv::Scalar(cfg_.red_h_max_2, 255, 255), m2); cv::bitwise_or(m1, m2, mask); } if (cfg_.morph_kernel > 0) { const cv::Mat k = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(cfg_.morph_kernel, cfg_.morph_kernel)); cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, k, cv::Point(-1, -1), cfg_.morph_iters); } mask_debug_ = mask.clone(); std::vector> contours; cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); std::vector bars; for (const auto &contour : contours) { const double area = cv::contourArea(contour); if (area < 20.0 || area > cfg_.max_area) continue; LightBar bar = fromContour(contour); if (bar.length < cfg_.min_length) continue; if (bar.width < 1.f) continue; if (bar.length / bar.width < cfg_.min_ratio) continue; if (bar.tilt_deg > cfg_.max_tilt_deg) continue; bars.push_back(bar); } return bars; } // ------------------------------------------------------------------ // PCA 角点修正(借鉴 jlu lightbar_corrector.cpp) // ------------------------------------------------------------------ std::optional LightBarDetector::findCorner( const cv::Mat &gray, const LightBar &bar, const cv::Point2f &axis_dir, const cv::Point2f ¢roid, float mean_val, bool toward_top) { const float L = bar.length; const float oper = toward_top ? 1.f : -1.f; const float dx = axis_dir.x * oper; const float dy = axis_dir.y * oper; std::vector candidates; const int n = static_cast(bar.width) - 2; const int half_n = static_cast(std::round(n / 2.0)); for (int i = -half_n; i <= half_n; ++i) { const float x0 = centroid.x + L * cfg_.search_start_ratio * dx + static_cast(i); const float y0 = centroid.y + L * cfg_.search_start_ratio * dy; cv::Point2f prev(x0, y0); cv::Point2f corner(x0, y0); float max_diff = 0.f; bool has_corner = false; for (float x = x0 + dx, y = y0 + dy; std::sqrt((x - x0) * (x - x0) + (y - y0) * (y - y0)) < L * (cfg_.search_end_ratio - cfg_.search_start_ratio); x += dx, y += dy) { const cv::Point2f cur(x, y); if (cur.x < 0 || cur.x >= gray.cols || cur.y < 0 || cur.y >= gray.rows) { break; } // 沿轴从亮(灯条内)走向暗(背景),亮度骤降处 = 端点 const float diff = static_cast(gray.at(static_cast(prev.y), static_cast(prev.x))) - static_cast(gray.at(static_cast(cur.y), static_cast(cur.x))); if (diff > max_diff && static_cast(gray.at(static_cast(prev.y), static_cast(prev.x))) > mean_val) { max_diff = diff; corner = prev; has_corner = true; } prev = cur; } if (has_corner) { candidates.push_back(corner); } } if (candidates.empty()) { return std::nullopt; } cv::Point2f sum(0.f, 0.f); for (const auto &p : candidates) { sum += p; } return sum / static_cast(candidates.size()); } bool LightBarDetector::correctBar(const cv::Mat &gray, LightBar &bar) { if (bar.width <= static_cast(cfg_.pass_optimize_lightbar_width)) { return false; } // 1) 扩大包围盒,留出搜索余量 cv::Rect box = bar.rect.boundingRect(); const int padx = static_cast(box.width * cfg_.padding_scale); const int pady = static_cast(box.height * cfg_.padding_scale); box.x -= padx; box.y -= pady; box.width += padx * 2; box.height += pady * 2; box.x = std::max(box.x, 0); box.y = std::max(box.y, 0); box.x = std::min(box.x, gray.cols - 1); box.y = std::min(box.y, gray.rows - 1); box.width = std::min(box.width, gray.cols - box.x); box.height = std::min(box.height, gray.rows - box.y); if (box.width <= 0 || box.height <= 0) { return false; } cv::Mat roi = gray(box); const float mean_val = static_cast(cv::mean(roi)[0]); if (mean_val <= cfg_.lightbar_min_mean_brightness) { return false; // 太暗,不是灯条 } cv::Mat roi_f; roi.convertTo(roi_f, CV_32F); cv::normalize(roi_f, roi_f, 0, cfg_.normalize_max_brightness, cv::NORM_MINMAX); // 2) 矩分析求对称轴(jlu:从点云 PCA 换成矩分析,更快) const cv::Moments m = cv::moments(roi_f, false); if (m.m00 == 0.0) { return false; } cv::Point2f centroid(static_cast(m.m10 / m.m00), static_cast(m.m01 / m.m00)); centroid += cv::Point2f(static_cast(box.x), static_cast(box.y)); const double mu20 = m.mu20, mu11 = m.mu11, mu02 = m.mu02; if (mu20 == 0.0 && mu11 == 0.0 && mu02 == 0.0) { return false; } const double theta = 0.5 * std::atan2(2.0 * mu11, mu20 - mu02); cv::Point2f axis(static_cast(std::cos(theta)), static_cast(std::sin(theta))); axis /= cv::norm(axis); if (axis.y > 0) { axis = -axis; // 指向上方(图像坐标 y 向下) } // 3) 沿轴找两端角点(最大亮度下降处) auto top = findCorner(gray, bar, axis, centroid, mean_val, true); auto bottom = findCorner(gray, bar, axis, centroid, mean_val, false); if (!top || !bottom) { return false; } bar.center = centroid; bar.axis = axis; bar.top = *top; bar.bottom = *bottom; bar.length = cv::norm(bar.top - bar.bottom); bar.signed_tilt = std::atan2(static_cast(bar.axis.x), -static_cast(bar.axis.y)); bar.pca_ok = true; return true; } // ------------------------------------------------------------------ // 灯条配对 → 装甲板 // ------------------------------------------------------------------ std::optional LightBarDetector::pairBars( const cv::Mat & /*gray*/, std::vector &bars) { ArmorTarget best; bool found = false; double best_score = 0.0; const int n = static_cast(bars.size()); for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { LightBar l = bars[i]; LightBar r = bars[j]; if (l.center.x > r.center.x) { std::swap(l, r); // l = 左条, r = 右条 } const float avg_len = (l.length + r.length) * 0.5f; const float dist = std::fabs(l.center.x - r.center.x); // ① 倾角差 const double angle_diff = std::fabs(l.signed_tilt - r.signed_tilt) * 180.0 / CV_PI; if (angle_diff > cfg_.max_angle_diff_deg) continue; // ② 长度比 const float len_ratio = std::max(l.length, r.length) / std::max(1e-3f, std::min(l.length, r.length)); if (len_ratio > cfg_.max_len_ratio) continue; // ③ 中心纵向差 if (std::fabs(l.center.y - r.center.y) > cfg_.max_center_y_ratio * avg_len) continue; // ④ 中心横向距离合理(小装甲 ≈2.4 倍灯条长,大装甲 ≈4.2 倍) if (dist < avg_len * 0.8f || dist > avg_len * 6.5f) continue; // ⑤ PCA 精修(开了就必须两边都成功,避免半精修导致 yaw 偏差) if (cfg_.use_pca && !(l.pca_ok && r.pca_ok)) continue; const double score = static_cast(avg_len) - std::fabs(l.center.y - r.center.y); if (!found || score > best_score) { found = true; best_score = score; best.left_light = l; best.right_light = r; best.center = (l.center + r.center) * 0.5f; // 角点顺序:板左下 → 板左上 → 板右上 → 板右下 // ⚠️ 镜像:板子朝向相机时,板的左条(+y)出现在画面右侧, // 所以「板左下」= 画面右条的底端(r = 画面右条) best.corners = {r.bottom, r.top, l.top, l.bottom}; best.is_big = (dist / avg_len) > cfg_.big_small_ratio_thres; } } } if (!found) { return std::nullopt; } return best; } // ------------------------------------------------------------------ // 主入口 // ------------------------------------------------------------------ std::optional LightBarDetector::detect(const cv::Mat &bgr) { if (bgr.empty()) { return std::nullopt; } bars_ = findBars(bgr); if (cfg_.use_pca) { cv::Mat gray; cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY); for (auto &b : bars_) { correctBar(gray, b); } } return pairBars(bgr, bars_); } // ------------------------------------------------------------------ // 调试绘制 // ------------------------------------------------------------------ void LightBarDetector::drawDebug(cv::Mat &img, const std::vector &bars, const std::optional &armor) { for (const auto &b : bars) { cv::line(img, b.bottom, b.top, cv::Scalar(0, 255, 255), 2); cv::circle(img, b.center, 3, cv::Scalar(0, 255, 255), -1); } if (!armor) { return; } const auto &a = *armor; for (int k = 0; k < 4; ++k) { cv::line(img, a.corners[k], a.corners[(k + 1) % 4], cv::Scalar(0, 255, 0), 2); } for (const auto &p : a.corners) { cv::circle(img, p, 4, cv::Scalar(0, 255, 0), -1); } cv::putText(img, a.is_big ? "BIG" : "SMALL", a.center + cv::Point2f(-30.f, -15.f), cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2); } } // namespace rm27