cmvr-es/cmvr-es/common/vision/image_display.h

610 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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