610 lines
20 KiB
C++
610 lines
20 KiB
C++
#pragma once
|
||
|
||
#include <algorithm>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include <opencv2/core.hpp>
|
||
#include <opencv2/highgui.hpp>
|
||
#include <opencv2/imgproc.hpp>
|
||
|
||
namespace cmvr::common {
|
||
|
||
/**
|
||
* @brief 图像像素坐标。
|
||
*
|
||
* 坐标系约定:
|
||
* - `u`:图像列坐标,向右为正,单位像素。
|
||
* - `v`:图像行坐标,向下为正,单位像素。
|
||
*/
|
||
struct Pixel {
|
||
int u{0}; // 图像像素列坐标,单位像素。
|
||
int v{0}; // 图像像素行坐标,单位像素。
|
||
};
|
||
|
||
/**
|
||
* @brief BGR 颜色定义。
|
||
*/
|
||
struct Color {
|
||
uint8_t b{0}; // 蓝色通道,范围 [0, 255]。
|
||
uint8_t g{255}; // 绿色通道,范围 [0, 255]。
|
||
uint8_t r{0}; // 红色通道,范围 [0, 255]。
|
||
|
||
/**
|
||
* @brief 转为 OpenCV 的 `cv::Scalar`。
|
||
*/
|
||
cv::Scalar toCvScalar() const {
|
||
return cv::Scalar(static_cast<double>(b),
|
||
static_cast<double>(g),
|
||
static_cast<double>(r));
|
||
}
|
||
|
||
/**
|
||
* @brief 预定义红色。
|
||
*/
|
||
static Color red() { return Color{0, 0, 255}; }
|
||
|
||
/**
|
||
* @brief 预定义绿色。
|
||
*/
|
||
static Color green() { return Color{0, 255, 0}; }
|
||
|
||
/**
|
||
* @brief 预定义蓝色。
|
||
*/
|
||
static Color blue() { return Color{255, 0, 0}; }
|
||
|
||
/**
|
||
* @brief 预定义黄色。
|
||
*/
|
||
static Color yellow() { return Color{0, 255, 255}; }
|
||
|
||
/**
|
||
* @brief 预定义白色。
|
||
*/
|
||
static Color white() { return Color{255, 255, 255}; }
|
||
|
||
/**
|
||
* @brief 预定义黑色。
|
||
*/
|
||
static Color black() { return Color{0, 0, 0}; }
|
||
};
|
||
|
||
/**
|
||
* @brief 图像上的文本叠加配置。
|
||
*/
|
||
struct TextOverlay {
|
||
std::string text; // 需要显示的字符串内容。
|
||
Pixel position_px{}; // 文本左下角在图像像素坐标系中的位置。
|
||
Color color{Color::green()}; // 文本颜色。
|
||
double font_scale{0.7}; // OpenCV 字体缩放系数。
|
||
int thickness{2}; // 文本线宽,单位像素。
|
||
int font_face{cv::FONT_HERSHEY_SIMPLEX}; // OpenCV 字体类型。
|
||
bool draw_background{false}; // 是否绘制文本背景框。
|
||
Color background_color{Color::black()}; // 文本背景框颜色。
|
||
int background_padding_px{2}; // 文本背景框四周留白,单位像素。
|
||
};
|
||
|
||
/**
|
||
* @brief 图像上的点叠加配置。
|
||
*/
|
||
struct PointOverlay {
|
||
Pixel position_px{}; // 点中心在图像像素坐标系中的位置。
|
||
Color color{Color::red()}; // 点颜色。
|
||
int radius_px{5}; // 圆点半径,单位像素。
|
||
int thickness{2}; // 线宽,`-1` 表示填充。
|
||
std::string label; // 点旁边附带显示的字符串;为空时不显示。
|
||
Pixel label_offset_px{8, -8}; // 标签相对点中心的像素偏移。
|
||
double label_font_scale{0.6}; // 点标签字体缩放系数。
|
||
int label_thickness{2}; // 点标签线宽,单位像素。
|
||
};
|
||
|
||
/**
|
||
* @brief 图像上的圆圈叠加配置。
|
||
*/
|
||
struct CircleOverlay {
|
||
Pixel center_px{}; // 圆心在图像像素坐标系中的位置。
|
||
int radius_px{10}; // 圆半径,单位像素。
|
||
Color color{Color::yellow()}; // 圆圈颜色。
|
||
int thickness{2}; // 圆圈线宽,单位像素,`-1` 表示填充。
|
||
};
|
||
|
||
/**
|
||
* @brief 图像上的十字叠加配置。
|
||
*/
|
||
struct CrossOverlay {
|
||
Pixel center_px{}; // 十字中心在图像像素坐标系中的位置。
|
||
int arm_length_px{8}; // 单侧十字臂长度,单位像素。
|
||
Color color{Color::blue()}; // 十字颜色。
|
||
int thickness{2}; // 十字线宽,单位像素。
|
||
};
|
||
|
||
/**
|
||
* @brief 通用图像叠加显示接口。
|
||
*
|
||
* 该接口不依赖任何感知或跟踪类,只抽象三类输入:
|
||
* - 待显示的图像数据;
|
||
* - 文本叠加项;
|
||
* - 点叠加项。
|
||
*/
|
||
class ImageDisplay {
|
||
public:
|
||
virtual ~ImageDisplay() = default;
|
||
|
||
/**
|
||
* @brief 设置窗口名称。
|
||
* @param window_name 显示窗口名称。
|
||
*/
|
||
virtual void setWindowName(const std::string& window_name) = 0;
|
||
|
||
/**
|
||
* @brief 设置待显示的图像。
|
||
* @param image 输入图像,支持灰度图、BGR 图或 BGRA 图。
|
||
*/
|
||
virtual void setImage(const cv::Mat& image) = 0;
|
||
|
||
/**
|
||
* @brief 清空所有叠加项,但保留当前图像。
|
||
*/
|
||
virtual void clearOverlays() = 0;
|
||
|
||
/**
|
||
* @brief 添加文本叠加项。
|
||
* @param overlay 文本叠加配置。
|
||
*/
|
||
virtual void showText(const TextOverlay& overlay) = 0;
|
||
|
||
/**
|
||
* @brief 按位置、字符串、颜色和大小直接添加文本。
|
||
* @param text 需要显示的字符串内容。
|
||
* @param position_px 文本左下角在图像像素坐标系中的位置。
|
||
* @param color 文本颜色。
|
||
* @param font_scale OpenCV 字体缩放系数。
|
||
* @param thickness 文本线宽,单位像素。
|
||
*/
|
||
virtual void showText(const std::string& text,
|
||
const Pixel& position_px,
|
||
const Color& color,
|
||
double font_scale = 0.7,
|
||
int thickness = 2) = 0;
|
||
|
||
/**
|
||
* @brief 添加点叠加项。
|
||
* @param overlay 点叠加配置。
|
||
*/
|
||
virtual void showPoint(const PointOverlay& overlay) = 0;
|
||
|
||
/**
|
||
* @brief 按位置、颜色和大小直接添加点。
|
||
* @param position_px 点中心在图像像素坐标系中的位置。
|
||
* @param color 点颜色。
|
||
* @param radius_px 圆点半径,单位像素。
|
||
* @param thickness 线宽,`-1` 表示填充。
|
||
* @param label 点旁边附带显示的字符串;为空时不显示。
|
||
*/
|
||
virtual void showPoint(const Pixel& position_px,
|
||
const Color& color,
|
||
int radius_px = 5,
|
||
int thickness = 2,
|
||
const std::string& label = {}) = 0;
|
||
|
||
/**
|
||
* @brief 显示圆圈叠加项。
|
||
* @param overlay 圆圈叠加配置。
|
||
*/
|
||
virtual void showCircle(const CircleOverlay& overlay) = 0;
|
||
|
||
/**
|
||
* @brief 按位置、半径、颜色和线宽直接显示圆圈。
|
||
* @param center_px 圆心在图像像素坐标系中的位置。
|
||
* @param radius_px 圆半径,单位像素。
|
||
* @param color 圆圈颜色。
|
||
* @param thickness 圆圈线宽,单位像素,`-1` 表示填充。
|
||
*/
|
||
virtual void showCircle(const Pixel& center_px,
|
||
int radius_px,
|
||
const Color& color,
|
||
int thickness = 2) = 0;
|
||
|
||
/**
|
||
* @brief 显示十字叠加项。
|
||
* @param overlay 十字叠加配置。
|
||
*/
|
||
virtual void showCross(const CrossOverlay& overlay) = 0;
|
||
|
||
/**
|
||
* @brief 按位置、尺寸、颜色和线宽直接显示十字。
|
||
* @param center_px 十字中心在图像像素坐标系中的位置。
|
||
* @param arm_length_px 单侧十字臂长度,单位像素。
|
||
* @param color 十字颜色。
|
||
* @param thickness 十字线宽,单位像素。
|
||
*/
|
||
virtual void showCross(const Pixel& center_px,
|
||
int arm_length_px,
|
||
const Color& color,
|
||
int thickness = 2) = 0;
|
||
|
||
/**
|
||
* @brief 判断当前是否已有待显示图像。
|
||
*/
|
||
virtual bool hasImage() const = 0;
|
||
|
||
/**
|
||
* @brief 将当前图像和叠加项渲染到输出图像。
|
||
* @param rendered_image 输出的渲染结果图像,BGR 三通道。
|
||
* @return 渲染成功返回 `true`。
|
||
*/
|
||
virtual bool render(cv::Mat& rendered_image) const = 0;
|
||
|
||
/**
|
||
* @brief 显示当前渲染结果。
|
||
* @param wait_key_ms `cv::waitKey` 等待时间,单位毫秒。
|
||
* @return 返回 `cv::waitKey` 的按键值;若没有可显示图像则返回 `-1`。
|
||
*/
|
||
virtual int show(int wait_key_ms = 1) = 0;
|
||
|
||
/**
|
||
* @brief 关闭显示窗口。
|
||
*/
|
||
virtual void close() = 0;
|
||
};
|
||
|
||
/**
|
||
* @brief 基于 OpenCV HighGUI 的图像叠加显示实现。
|
||
*/
|
||
class OpenCvImageCvDisplay final : public ImageDisplay {
|
||
public:
|
||
/**
|
||
* @brief 构造显示器。
|
||
* @param window_name 显示窗口名称。
|
||
*/
|
||
explicit OpenCvImageCvDisplay(std::string window_name = "image_overlay")
|
||
: window_name_(std::move(window_name)) {}
|
||
|
||
/**
|
||
* @brief 设置窗口名称。
|
||
* @param window_name 显示窗口名称。
|
||
*/
|
||
void setWindowName(const std::string& window_name) override {
|
||
window_name_ = window_name;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置待显示图像。
|
||
* @param image 输入图像,支持灰度图、BGR 图或 BGRA 图。
|
||
*/
|
||
void setImage(const cv::Mat& image) override {
|
||
image_ = image.clone();
|
||
}
|
||
|
||
/**
|
||
* @brief 清空所有叠加项,但保留当前图像。
|
||
*/
|
||
void clearOverlays() override {
|
||
texts_.clear();
|
||
points_.clear();
|
||
circles_.clear();
|
||
crosses_.clear();
|
||
}
|
||
|
||
/**
|
||
* @brief 添加文本叠加项。
|
||
* @param overlay 文本叠加配置。
|
||
*/
|
||
void showText(const TextOverlay& overlay) override {
|
||
texts_.push_back(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 按位置、字符串、颜色和大小直接添加文本。
|
||
* @param text 需要显示的字符串内容。
|
||
* @param position_px 文本左下角在图像像素坐标系中的位置。
|
||
* @param color 文本颜色。
|
||
* @param font_scale OpenCV 字体缩放系数。
|
||
* @param thickness 文本线宽,单位像素。
|
||
*/
|
||
void showText(const std::string& text,
|
||
const Pixel& position_px,
|
||
const Color& color,
|
||
double font_scale = 0.7,
|
||
int thickness = 2) override {
|
||
TextOverlay overlay;
|
||
overlay.text = text;
|
||
overlay.position_px = position_px;
|
||
overlay.color = color;
|
||
overlay.font_scale = font_scale;
|
||
overlay.thickness = thickness;
|
||
showText(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 添加点叠加项。
|
||
* @param overlay 点叠加配置。
|
||
*/
|
||
void showPoint(const PointOverlay& overlay) override {
|
||
points_.push_back(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 按位置、颜色和大小直接添加点。
|
||
* @param position_px 点中心在图像像素坐标系中的位置。
|
||
* @param color 点颜色。
|
||
* @param radius_px 圆点半径,单位像素。
|
||
* @param thickness 线宽,`-1` 表示填充。
|
||
* @param label 点旁边附带显示的字符串;为空时不显示。
|
||
*/
|
||
void showPoint(const Pixel& position_px,
|
||
const Color& color,
|
||
int radius_px = 5,
|
||
int thickness = 2,
|
||
const std::string& label = {}) override {
|
||
PointOverlay overlay;
|
||
overlay.position_px = position_px;
|
||
overlay.color = color;
|
||
overlay.radius_px = radius_px;
|
||
overlay.thickness = thickness;
|
||
overlay.label = label;
|
||
showPoint(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 显示圆圈叠加项。
|
||
* @param overlay 圆圈叠加配置。
|
||
*/
|
||
void showCircle(const CircleOverlay& overlay) override {
|
||
circles_.push_back(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 按位置、半径、颜色和线宽直接显示圆圈。
|
||
* @param center_px 圆心在图像像素坐标系中的位置。
|
||
* @param radius_px 圆半径,单位像素。
|
||
* @param color 圆圈颜色。
|
||
* @param thickness 圆圈线宽,单位像素,`-1` 表示填充。
|
||
*/
|
||
void showCircle(const Pixel& center_px,
|
||
int radius_px,
|
||
const Color& color,
|
||
int thickness = 2) override {
|
||
CircleOverlay overlay;
|
||
overlay.center_px = center_px;
|
||
overlay.radius_px = radius_px;
|
||
overlay.color = color;
|
||
overlay.thickness = thickness;
|
||
showCircle(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 显示十字叠加项。
|
||
* @param overlay 十字叠加配置。
|
||
*/
|
||
void showCross(const CrossOverlay& overlay) override {
|
||
crosses_.push_back(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 按位置、尺寸、颜色和线宽直接显示十字。
|
||
* @param center_px 十字中心在图像像素坐标系中的位置。
|
||
* @param arm_length_px 单侧十字臂长度,单位像素。
|
||
* @param color 十字颜色。
|
||
* @param thickness 十字线宽,单位像素。
|
||
*/
|
||
void showCross(const Pixel& center_px,
|
||
int arm_length_px,
|
||
const Color& color,
|
||
int thickness = 2) override {
|
||
CrossOverlay overlay;
|
||
overlay.center_px = center_px;
|
||
overlay.arm_length_px = arm_length_px;
|
||
overlay.color = color;
|
||
overlay.thickness = thickness;
|
||
showCross(overlay);
|
||
}
|
||
|
||
/**
|
||
* @brief 判断当前是否已有待显示图像。
|
||
*/
|
||
bool hasImage() const override {
|
||
return !image_.empty();
|
||
}
|
||
|
||
/**
|
||
* @brief 将当前图像和叠加项渲染到输出图像。
|
||
* @param rendered_image 输出的渲染结果图像,BGR 三通道。
|
||
* @return 渲染成功返回 `true`。
|
||
*/
|
||
bool render(cv::Mat& rendered_image) const override {
|
||
if (!toBgrImage(image_, rendered_image)) {
|
||
return false;
|
||
}
|
||
|
||
for (const CircleOverlay& overlay : circles_) {
|
||
drawCircle(rendered_image, overlay);
|
||
}
|
||
for (const CrossOverlay& overlay : crosses_) {
|
||
drawCross(rendered_image, overlay);
|
||
}
|
||
for (const PointOverlay& overlay : points_) {
|
||
drawPoint(rendered_image, overlay);
|
||
}
|
||
for (const TextOverlay& overlay : texts_) {
|
||
drawText(rendered_image, overlay);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 显示当前渲染结果。
|
||
* @param wait_key_ms `cv::waitKey` 等待时间,单位毫秒。
|
||
* @return 返回 `cv::waitKey` 的按键值;若没有可显示图像则返回 `-1`。
|
||
*/
|
||
int show(int wait_key_ms = 1) override {
|
||
cv::Mat rendered_image;
|
||
if (!render(rendered_image)) {
|
||
return -1;
|
||
}
|
||
|
||
if (!window_created_) {
|
||
cv::namedWindow(window_name_, cv::WINDOW_NORMAL);
|
||
window_created_ = true;
|
||
}
|
||
cv::imshow(window_name_, rendered_image);
|
||
return cv::waitKey(wait_key_ms);
|
||
}
|
||
|
||
/**
|
||
* @brief 关闭显示窗口。
|
||
*/
|
||
void close() override {
|
||
if (!window_created_) {
|
||
return;
|
||
}
|
||
cv::destroyWindow(window_name_);
|
||
window_created_ = false;
|
||
}
|
||
|
||
private:
|
||
/**
|
||
* @brief 将输入图像转换为可叠加绘制的 BGR 三通道图像。
|
||
* @param input 输入图像。
|
||
* @param output 输出 BGR 图像。
|
||
* @return 转换成功返回 `true`。
|
||
*/
|
||
static bool toBgrImage(const cv::Mat& input, cv::Mat& output) {
|
||
if (input.empty()) {
|
||
return false;
|
||
}
|
||
|
||
if (input.channels() == 1) {
|
||
cv::cvtColor(input, output, cv::COLOR_GRAY2BGR);
|
||
return true;
|
||
}
|
||
if (input.channels() == 3) {
|
||
output = input.clone();
|
||
return true;
|
||
}
|
||
if (input.channels() == 4) {
|
||
cv::cvtColor(input, output, cv::COLOR_BGRA2BGR);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @brief 在图像上绘制文本叠加项。
|
||
* @param image 目标图像,BGR 三通道。
|
||
* @param overlay 文本叠加配置。
|
||
*/
|
||
static void drawText(cv::Mat& image, const TextOverlay& overlay) {
|
||
const cv::Point origin(overlay.position_px.u, overlay.position_px.v);
|
||
const int thickness = std::max(1, overlay.thickness);
|
||
const double font_scale = std::max(0.0, overlay.font_scale);
|
||
|
||
if (overlay.draw_background) {
|
||
int baseline = 0;
|
||
const cv::Size text_size = cv::getTextSize(overlay.text,
|
||
overlay.font_face,
|
||
font_scale,
|
||
thickness,
|
||
&baseline);
|
||
const int pad = std::max(0, overlay.background_padding_px);
|
||
const cv::Point top_left(origin.x - pad, origin.y - text_size.height - pad);
|
||
const cv::Point bottom_right(origin.x + text_size.width + pad, origin.y + baseline + pad);
|
||
cv::rectangle(image,
|
||
top_left,
|
||
bottom_right,
|
||
overlay.background_color.toCvScalar(),
|
||
cv::FILLED);
|
||
}
|
||
|
||
cv::putText(image,
|
||
overlay.text,
|
||
origin,
|
||
overlay.font_face,
|
||
font_scale,
|
||
overlay.color.toCvScalar(),
|
||
thickness,
|
||
cv::LINE_AA);
|
||
}
|
||
|
||
/**
|
||
* @brief 在图像上绘制点叠加项。
|
||
* @param image 目标图像,BGR 三通道。
|
||
* @param overlay 点叠加配置。
|
||
*/
|
||
static void drawPoint(cv::Mat& image, const PointOverlay& overlay) {
|
||
const cv::Point center(overlay.position_px.u, overlay.position_px.v);
|
||
const int radius = std::max(1, overlay.radius_px);
|
||
cv::circle(image,
|
||
center,
|
||
radius,
|
||
overlay.color.toCvScalar(),
|
||
overlay.thickness,
|
||
cv::LINE_AA);
|
||
|
||
if (!overlay.label.empty()) {
|
||
TextOverlay label_overlay;
|
||
label_overlay.text = overlay.label;
|
||
label_overlay.position_px = Pixel{center.x + overlay.label_offset_px.u,
|
||
center.y + overlay.label_offset_px.v};
|
||
label_overlay.color = overlay.color;
|
||
label_overlay.font_scale = overlay.label_font_scale;
|
||
label_overlay.thickness = overlay.label_thickness;
|
||
drawText(image, label_overlay);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 在图像上绘制圆圈叠加项。
|
||
* @param image 目标图像,BGR 三通道。
|
||
* @param overlay 圆圈叠加配置。
|
||
*/
|
||
static void drawCircle(cv::Mat& image, const CircleOverlay& overlay) {
|
||
const cv::Point center(overlay.center_px.u, overlay.center_px.v);
|
||
const int radius = std::max(1, overlay.radius_px);
|
||
const int thickness = (overlay.thickness == 0) ? 1 : overlay.thickness;
|
||
cv::circle(image,
|
||
center,
|
||
radius,
|
||
overlay.color.toCvScalar(),
|
||
thickness,
|
||
cv::LINE_AA);
|
||
}
|
||
|
||
/**
|
||
* @brief 在图像上绘制十字叠加项。
|
||
* @param image 目标图像,BGR 三通道。
|
||
* @param overlay 十字叠加配置。
|
||
*/
|
||
static void drawCross(cv::Mat& image, const CrossOverlay& overlay) {
|
||
const cv::Point center(overlay.center_px.u, overlay.center_px.v);
|
||
const int arm_length = std::max(1, overlay.arm_length_px);
|
||
const int thickness = std::max(1, overlay.thickness);
|
||
const cv::Scalar color = overlay.color.toCvScalar();
|
||
cv::line(image,
|
||
cv::Point(center.x - arm_length, center.y),
|
||
cv::Point(center.x + arm_length, center.y),
|
||
color,
|
||
thickness,
|
||
cv::LINE_AA);
|
||
cv::line(image,
|
||
cv::Point(center.x, center.y - arm_length),
|
||
cv::Point(center.x, center.y + arm_length),
|
||
color,
|
||
thickness,
|
||
cv::LINE_AA);
|
||
}
|
||
|
||
std::string window_name_; // 显示窗口名称。
|
||
cv::Mat image_; // 当前待显示图像。
|
||
std::vector<TextOverlay> texts_; // 当前待绘制的文本叠加列表。
|
||
std::vector<PointOverlay> points_; // 当前待绘制的点叠加列表。
|
||
std::vector<CircleOverlay> circles_; // 当前待绘制的圆圈叠加列表。
|
||
std::vector<CrossOverlay> crosses_; // 当前待绘制的十字叠加列表。
|
||
bool window_created_{false}; // 显示窗口是否已经创建。
|
||
};
|
||
|
||
} // namespace cmvr::common
|