自瞄教程 / 可运行示例
第一个传统视觉识别器
此页面随教程站静态发布,不依赖 GitHub 仓库权限。下面是examples/first-traditional-detector 的全部三份源码;可直接复制到本地同名目录后,按教程中的 CMake 命令构建。
需要先下载与教程匹配的 Daedalus Simulator Release,并让CMAKE_PREFIX_PATH 指向其 sdk 目录。
README.md
# 第一个传统视觉识别器
这是“自瞄教程”识别模块的可运行传统视觉参考程序。它连接 Daedalus Simulator、切换
Shooting Range、读取最新图像,并完成颜色分割、轮廓筛选、灯条配对与角点绘制;不识别数字。
构建前安装 OpenCV 4,并从 [Daedalus Simulator Releases](https://github.com/biqibao112212-bot/daedalus-simulator-releases) 下载对应平台的最新稳定完整 Release。模拟器、SDK、标定和文档必须来自同一个包,具体版本以包内 `release.json` 为准。
```bash
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=<Daedalus发布目录>/sdk
cmake --build build --parallel
```
Windows 使用 vcpkg 时,同时传入:
```text
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake
```
运行模拟器后启动 `first_traditional_detector`。按 `1` 切换静止目标,按 `2` 切换直线
运动,按 `3` 切换直线并自转,按 `Q` 或 `Esc` 退出。`detector` 窗口中:橙色框为候选灯条、
黄色线为灯条长轴、绿色四边形和圆点为两灯条配对得到的角点;`mask` 窗口显示二值图。
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(first_traditional_detector LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(DaedalusSimSdk 1 REQUIRED CONFIG)
find_package(OpenCV 4 REQUIRED COMPONENTS core imgproc highgui)
add_executable(first_traditional_detector src/main.cpp)
target_link_libraries(first_traditional_detector PRIVATE
DaedalusSimSdk::DaedalusSimSdk
${OpenCV_LIBS}
)
target_include_directories(first_traditional_detector PRIVATE ${OpenCV_INCLUDE_DIRS})
src/main.cpp
#include <daedalus_sim_sdk/scene_control_client.hpp>
#include <daedalus_sim_sdk/tcp_image_client.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using namespace daedalus::sim::sdk::v1;
struct DetectorParameters {
int brightness_threshold = 180;
int color_difference_threshold = 40;
int morphology_size = 1;
int minimum_contour_area = 8;
int enemy_is_blue = 0;
};
struct LightBar {
cv::RotatedRect rect;
cv::Point2f top;
cv::Point2f bottom;
float length = 0.0F;
float angle_deg = 0.0F;
};
struct DetectionResult {
cv::Mat mask;
std::vector<LightBar> light_bars;
// 顺序固定为:左上、右上、右下、左下。
std::vector<std::array<cv::Point2f, 4>> armor_corners;
};
float normalizeAngle180(float angle_deg) {
angle_deg = std::fmod(angle_deg, 180.0F);
return angle_deg < 0.0F ? angle_deg + 180.0F : angle_deg;
}
float angleDifference180(float lhs_deg, float rhs_deg) {
const float difference = std::abs(normalizeAngle180(lhs_deg) -
normalizeAngle180(rhs_deg));
return std::min(difference, 180.0F - difference);
}
LightBar makeLightBar(const cv::RotatedRect& rect) {
LightBar bar;
bar.rect = rect;
bar.length = std::max(rect.size.width, rect.size.height);
bar.angle_deg = rect.angle;
if (rect.size.width > rect.size.height) {
bar.angle_deg += 90.0F;
}
const float angle_rad = bar.angle_deg * static_cast<float>(CV_PI / 180.0);
const cv::Point2f direction(std::cos(angle_rad), std::sin(angle_rad));
const cv::Point2f endpoint_a = rect.center + direction * (bar.length * 0.5F);
const cv::Point2f endpoint_b = rect.center - direction * (bar.length * 0.5F);
if (endpoint_a.y < endpoint_b.y) {
bar.top = endpoint_a;
bar.bottom = endpoint_b;
} else {
bar.top = endpoint_b;
bar.bottom = endpoint_a;
}
return bar;
}
DetectionResult runTraditionalDetector(const cv::Mat& bgr,
const DetectorParameters& parameters) {
DetectionResult result;
std::vector<cv::Mat> channels;
cv::split(bgr, channels);
cv::Mat color_score;
if (parameters.enemy_is_blue != 0) {
cv::subtract(channels[0], channels[2], color_score); // B - R
} else {
cv::subtract(channels[2], channels[0], color_score); // R - B
}
cv::Mat gray;
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
cv::Mat color_mask;
cv::Mat brightness_mask;
cv::threshold(color_score, color_mask, parameters.color_difference_threshold,
255, cv::THRESH_BINARY);
cv::threshold(gray, brightness_mask, parameters.brightness_threshold, 255,
cv::THRESH_BINARY);
cv::bitwise_and(color_mask, brightness_mask, result.mask);
const int radius = std::max(parameters.morphology_size, 0);
if (radius > 0) {
const int size = radius * 2 + 1;
const cv::Mat kernel = cv::getStructuringElement(
cv::MORPH_ELLIPSE, cv::Size(size, size));
cv::morphologyEx(result.mask, result.mask, cv::MORPH_CLOSE, kernel);
}
std::vector<std::vector<cv::Point>> contours;
cv::findContours(result.mask, contours, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
for (const std::vector<cv::Point>& contour : contours) {
const double area = cv::contourArea(contour);
if (area < static_cast<double>(parameters.minimum_contour_area) ||
contour.size() < 5) {
continue;
}
const cv::RotatedRect rect = cv::minAreaRect(contour);
const float length = std::max(rect.size.width, rect.size.height);
const float width = std::max(1.0F, std::min(rect.size.width, rect.size.height));
if (length < 4.0F || length / width < 1.8F) {
continue;
}
result.light_bars.push_back(makeLightBar(rect));
}
struct PairCandidate {
int left = 0;
int right = 0;
float score = 0.0F;
};
std::vector<PairCandidate> pairs;
for (int first = 0; first < static_cast<int>(result.light_bars.size()); ++first) {
for (int second = first + 1;
second < static_cast<int>(result.light_bars.size()); ++second) {
int left = first;
int right = second;
if (result.light_bars[left].rect.center.x > result.light_bars[right].rect.center.x) {
std::swap(left, right);
}
const LightBar& lhs = result.light_bars[left];
const LightBar& rhs = result.light_bars[right];
const float average_length = (lhs.length + rhs.length) * 0.5F;
const float length_ratio = std::max(lhs.length, rhs.length) /
std::max(1.0F, std::min(lhs.length, rhs.length));
const float angle_difference = angleDifference180(lhs.angle_deg, rhs.angle_deg);
const float height_difference =
std::abs(lhs.rect.center.y - rhs.rect.center.y) / average_length;
const float separation =
std::abs(lhs.rect.center.x - rhs.rect.center.x) / average_length;
if (length_ratio > 1.6F || angle_difference > 20.0F ||
height_difference > 0.65F || separation < 0.6F || separation > 6.0F) {
continue;
}
pairs.push_back({left, right, length_ratio + angle_difference / 20.0F +
height_difference});
}
}
std::sort(pairs.begin(), pairs.end(), [](const PairCandidate& lhs,
const PairCandidate& rhs) {
return lhs.score < rhs.score;
});
std::vector<bool> used(result.light_bars.size(), false);
for (const PairCandidate& pair : pairs) {
if (used[pair.left] || used[pair.right]) {
continue;
}
const LightBar& left = result.light_bars[pair.left];
const LightBar& right = result.light_bars[pair.right];
result.armor_corners.push_back(
{left.top, right.top, right.bottom, left.bottom});
used[pair.left] = true;
used[pair.right] = true;
}
return result;
}
bool sceneSucceeded(const ClientResult<SceneControlResponse>& response,
std::string_view operation) {
if (!response) {
std::cerr << operation << " failed: " << response.status.message << '\n';
return false;
}
if (response.value->status != SceneControlStatus::Ok) {
std::cerr << operation << " rejected: " << response.value->message << '\n';
return false;
}
return true;
}
bool setTargetMotion(SceneControlClient& scene, RangeMotionMode mode) {
RangeTargetMotion motion;
motion.target = 3;
motion.mode = mode;
motion.direction_deg = 90.0F;
motion.linear_speed_mps = mode == RangeMotionMode::Stationary ? 0.0F : 1.5F;
motion.linear_span_m = mode == RangeMotionMode::Stationary ? 0.0F : 4.0F;
motion.spin_deg_s = mode == RangeMotionMode::LinearAndSpin ? 60.0F : 0.0F;
return sceneSucceeded(scene.setRangeTargetMotion(motion), "setRangeTargetMotion");
}
void drawLightBar(cv::Mat& display, const LightBar& bar) {
cv::Point2f vertices[4];
bar.rect.points(vertices);
for (int index = 0; index < 4; ++index) {
cv::line(display, vertices[index], vertices[(index + 1) % 4],
cv::Scalar(0, 165, 255), 2, cv::LINE_AA);
}
cv::line(display, bar.top, bar.bottom, cv::Scalar(0, 255, 255), 2,
cv::LINE_AA);
}
int main() {
SceneControlOptions scene_options;
scene_options.session_id = "first-traditional-detector";
SceneControlClient scene(scene_options);
if (!sceneSucceeded(scene.createSession(), "createSession") ||
!sceneSucceeded(scene.setScene(SceneMode::ShootingRange), "setScene") ||
!sceneSucceeded(scene.resetScene(), "resetScene") ||
!setTargetMotion(scene, RangeMotionMode::Stationary)) {
return 1;
}
TcpImageClient images;
const ClientStatus connected = images.connect();
if (!connected) {
std::cerr << "connect image stream failed: " << connected.message << '\n';
return 2;
}
DetectorParameters parameters;
cv::namedWindow("controls", cv::WINDOW_NORMAL);
cv::namedWindow("detector", cv::WINDOW_NORMAL);
cv::namedWindow("mask", cv::WINDOW_NORMAL);
cv::createTrackbar("brightness", "controls",
¶meters.brightness_threshold, 255);
cv::createTrackbar("color difference", "controls",
¶meters.color_difference_threshold, 255);
cv::createTrackbar("morphology radius", "controls",
¶meters.morphology_size, 10);
cv::createTrackbar("minimum area", "controls",
¶meters.minimum_contour_area, 500);
cv::createTrackbar("enemy: red / blue", "controls",
¶meters.enemy_is_blue, 1);
std::uint64_t previous_sequence = 0;
while (true) {
auto frame = images.waitForLatest(previous_sequence,
std::chrono::milliseconds(1000));
if (!frame) {
if (frame.status.error != ClientError::Timeout) {
std::cerr << "waitForLatest failed: " << frame.status.message << '\n';
}
continue;
}
previous_sequence = frame.value->header.source_sequence;
const auto& source = *frame.value;
const int type = source.header.format == tcp_image::PixelFormat::Rgba32
? CV_8UC4
: CV_8UC3;
cv::Mat view(static_cast<int>(source.header.height),
static_cast<int>(source.header.width), type,
const_cast<std::uint8_t*>(source.payload.data()));
cv::Mat bgr;
if (source.header.format == tcp_image::PixelFormat::Rgba32) {
cv::cvtColor(view, bgr, cv::COLOR_RGBA2BGR);
} else {
cv::cvtColor(view, bgr, cv::COLOR_RGB2BGR);
}
DetectionResult detection = runTraditionalDetector(bgr, parameters);
cv::Mat display = bgr.clone();
for (const LightBar& light_bar : detection.light_bars) {
drawLightBar(display, light_bar);
}
for (const auto& corners : detection.armor_corners) {
std::vector<cv::Point> polygon;
polygon.reserve(corners.size());
for (const cv::Point2f& corner : corners) {
polygon.emplace_back(cvRound(corner.x), cvRound(corner.y));
cv::circle(display, corner, 4, cv::Scalar(0, 255, 0), cv::FILLED,
cv::LINE_AA);
}
cv::polylines(display, polygon, true, cv::Scalar(0, 255, 0), 2,
cv::LINE_AA);
}
const std::string identity =
"seq=" + std::to_string(source.header.source_sequence) +
" timestamp_ns=" + std::to_string(source.header.capture_timestamp_ns);
const std::string summary =
"light bars=" + std::to_string(detection.light_bars.size()) +
" armor pairs=" + std::to_string(detection.armor_corners.size());
cv::putText(display, identity, {20, 35}, cv::FONT_HERSHEY_SIMPLEX, 0.7,
cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
cv::putText(display, summary, {20, 65}, cv::FONT_HERSHEY_SIMPLEX, 0.7,
cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
cv::imshow("detector", display);
cv::imshow("mask", detection.mask);
const int key = cv::waitKey(1) & 0xff;
if (key == 27 || key == 'q') break;
if (key == '1') setTargetMotion(scene, RangeMotionMode::Stationary);
if (key == '2') setTargetMotion(scene, RangeMotionMode::Linear);
if (key == '3') setTargetMotion(scene, RangeMotionMode::LinearAndSpin);
}
images.close();
return 0;
}