当前位置: 首页 > article >正文

QT-简单视觉框架代码

文章目录

  • 简介
  • 1. 整体架构
  • 2. 关键类功能概述
  • 3. 详细代码实现
  • hikcameraworker.h 和 hikcameraworker.cpp(海康相机工作线程类)
  • imageviewerwidget.h 和 imageviewerwidget.cpp(图像查看部件类)
      • 构造函数 `ImageViewerWidget`
      • 析构函数 `~ImageViewerWidget`
      • `updateImage`
      • `addToolAction`
      • `removeToolAction`
      • `mousePressEvent`
      • `mouseMoveEvent`
      • `mouseReleaseEvent`
      • `wheelEvent`
      • `drawRectangle`
      • `handleRightClickMenu`
      • `zoomIn`
      • `zoomOut`
      • `translateView`
      • `startLineMeasurement`
      • `continueLineMeasurement`
      • `finishLineMeasurement`
      • `updateLineGeometry`
      • `isNearLineEndpoint`
      • `getLineEndpointIndex`
      • `updateLineRotation`
      • `startCircleMeasurement`
      • `continueCircleMeasurement`
      • `finishCircleMeasurement`
      • `startRotatedRectMeasurement`
      • `continueRotatedRectMeasurement`
      • `finishRotatedRectMeasurement`
      • `isNearCorner`
      • `getCornerIndex`
      • `updateRectGeometry`
      • `updateRectRotation`
      • `showLineTooltip`
      • `showRectTooltip`
      • `showCircleTooltip`
  • cameramanager.h 和 cameramanager.cpp(相机管理类,负责相机业务逻辑)
  • mainwindow.h 和 mainwindow.cpp(主窗口类)
  • main.cpp(程序入口)
  • 4. 功能说明

简介

以下是一个满足需求的 Qt 程序示例,采用 C++ QT语言编写,通过合理的分层架构设计,实现了界面与业务逻辑的分离,具备对多台海康相机的高效控制以及丰富的交互功能:

1. 整体架构

程序分为三层:界面层(负责显示和用户交互)、业务逻辑层(处理相机相关操作)、数据模型层(存储相机数据及状态,本示例未详细展开,可按需扩展)。通过信号与槽机制在各层之间传递信息,保证模块的低耦合性。

2. 关键类功能概述

  • CameraInterface:定义相机操作的抽象接口,确保不同相机厂商实现的兼容性。
    HikCameraWorker:继承自CameraInterface,实现海康相机的具体业务逻辑,包括连接、取图、断线重连、手动触发等。
  • MainWindow:主窗口类,负责创建界面,集成菜单栏、工具栏、状态栏、图像显示区和日志区,响应用户操作并与业务逻辑层交互。
  • ImageViewerWidget:自定义图像显示部件,封装 QGraphicsView 和QGraphicsScene,负责接收并展示图像数据。

3. 详细代码实现

  • camerainterface.h(相机接口抽象类)
#ifndef CAMERAINTERFACE_H
#define CAMERAINTERFACE_H

#include <QObject>

class CameraInterface : public QObject
{
    Q_OBJECT
public:
    virtual ~CameraInterface() {}

    // 初始化相机,返回是否成功
    virtual bool initialize() = 0;
    // 启动相机图像采集
    virtual void startCapture() = 0;
    // 停止相机图像采集
    virtual void stopCapture() = 0;
    // 手动触发一次图像采集
    virtual void manualTrigger() = 0;
    // 断开相机连接
    virtual void disconnectCamera() = 0;
    // 获取相机当前连接状态
    virtual bool isConnected() const = 0;

signals:
    // 图像采集成功信号,携带图像数据、宽度、高度
    void imageCaptured(unsigned char* imageData, int width, int height);
    // 相机出错信号
    void cameraError();
};

#endif // CAMERAINTERFACE_H

hikcameraworker.h 和 hikcameraworker.cpp(海康相机工作线程类)

// hikcameraworker.h
#ifndef HIKCAMERAWORKER_H
#define HIKCAMERAWORKER_H

#include <QObject>
#include <QThread>
#include <MvCameraControl.h>
#include "camerainterface.h"

class HikCameraWorker : public CameraInterface
{
    Q_OBJECT
public:
    explicit HikCameraWorker(int cameraIndex, QObject *parent = nullptr);
    ~HikCameraWorker();

private:
    int m_cameraIndex;
    MV_CC_DEVICE_INFO_LIST m_deviceInfoList;
    MV_CC_DEVICE_INFO* m_pDeviceInfo;
    MV_CC_HANDLE m_cameraHandle;
    bool m_isCapturing;
    bool m_isConnected;

    bool connectCamera();
    void disconnectCamera();
    static void __stdcall imageCallback(MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser);

    // CameraInterface 接口实现
    bool initialize() override;
    void startCapture() override;
    void stopCapture() override;
    void manualTrigger() override;
    void disconnectCamera() override;
    bool isConnected() const override;
};

// hikcameraworker.cpp
#include "hikcameraworker.h"

HikCameraWorker::HikCameraWorker(int cameraIndex, QObject *parent) :
    CameraInterface(parent),
    m_cameraIndex(cameraIndex),
    m_pDeviceInfo(null nullable),
    m_cameraHandle(null nullable),
    m_isCapturing(false),
    m_isConnected(false)
{
    // 枚举设备
    MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &m_deviceInfoList);
    if (m_deviceIdex >= 0 && m_deviceIdex < m_deviceInfoList.nDeviceNum) {
        m_pDeviceInfo = m_deviceInfoList.pDeviceInfo[m_deviceIndex];
    }
}

HikCameraWorker::~HikCameraWorker()
{
    stopCapture();
    disconnectCamera();
}

bool HikCameraWorker::initialize()
{
    return connectCamera();
}

void HikCameraWorker::startCapture()
{
    if (!m_isConnected &&!connectCamera()) {
        emit cameraError();
        return;
    }

    m_isCapturing = true;
    MV_CC_SetCallbackFunction(m_cameraHandle, imageCallback, this);
    MV_CC_StartGrabbing(m_cameraHandle);
}

void HikCameraWorker::stopCapture()
{
    m_isCapturing = false;
    if (m_cameraHandle) {
        MV_CC_StopGrabbing(m_cameraHandle);
        MV_CC_DestroyHandle(m_cameraHandle);
        m_cameraHandle = nullptr;
    }
}

void HikCameraWorker::manualTrigger()
{
    if (m_isConnected && m_cameraHandle) {
        MV_CC_SoftwareTriggerCommand(m_cameraHandle);
    }
}

bool HikCameraWorker::connectCamera()
{
    if (m_pDeviceInfo) {
        int nRet = MV_CC_CreateHandle(&m_cameraHandle, m_pDeviceInfo);
        if (nRet == MV_OK) {
            nRet = MV_CC_OpenDevice(m_cameraHandle);
            if (nRet == MV_OK) {
                m_isConnected = true;
                return true;
            } else {
                MV_CC_DestroyHandle(m_cameraHandle);
                char errorMsg[1024];
                MV_CC_GetLastErrorMsg(errorMsg, sizeof(errorMsg));
                qDebug() << "Open device error: " << errorMsg;
                m_cameraHandle = nullptr;
            }
        }
    }
    return false;
}

void HikCameraWorker::disconnectCamera()
{
    if (m_cameraHandle) {
        MV_CC_CloseDevice(m_cameraHandle);
        MV_CC_DestroyHandle(m_cameraHandle);
        m_cameraHandle = nullptr;
    }
    m_isConnected = false;
}

void __stdcall HikCameraWorker::imageCallback(MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser)
{
    HikCameraWorker* worker = static_cast<HikCameraWorker*>(pUser);
    if (worker && worker->m_isCapturing) {
        emit worker->imageCaptured(pFrameInfo->pBuf, pFrameInfo->nWidth, pFrameInfo->nHeight);
    }
}

bool HikCameraWorker::isConnected() const
{
    return m_isConnected;
}

imageviewerwidget.h 和 imageviewerwidget.cpp(图像查看部件类)

// imageviewerwidget.h
#ifndef IMAGEVIEWERWIDGET_H
#define IMAGEVIEWERWIDGET_H

#include <QWidget>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QGraphicsRectItem>
#include <QGraphicsItemGroup>
#include <QMenu>
#include <QAction>
#include <QDebug>
#include <QPainterPath>
#include <cmath>
#include <QToolTip>

class ImageViewerWidget : public QWidget
{
    Q_OBJECT
public:
    explicit ImageViewerWidget(QWidget *parent = nullptr);
    ~ImageViewerWidget();

    // 动态添加工具项到右键菜单,例如找线卡尺、圆形卡尺等
    void addToolAction(QAction* action);
    // 动态移除工具项
    void removeToolAction(QAction* action);

public slots:
    void updateImage(unsigned char* imageData, int width, int height);

signals:
    // 当绘制矩形等操作完成后,发出信号通知外界(例如主窗口),携带矩形信息
    void shapeDrawn(QRectF rect);
    // 当使用找线卡尺完成测量后,发出信号携带线的相关信息(起点、终点坐标等)
    void lineMeasured(QPointF start, QPointF end);
    // 当使用圆形卡尺完成测量后,发出信号携带圆的相关信息(圆心、半径)
    void circleMeasured(QPointF center, double radius);
    // 当绘制旋转矩形完成后,发出信号携带旋转矩形相关信息(矩形、旋转角度)
    void rotatedRectDrawn(QRectF rect, double angle);

protected:
    void mousePressEvent(QMouseEvent* event) override;
    void mouseMoveEvent(QMouseEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* event) override;
    void wheelEvent(QWheelEvent* event) override;

private:
    QGraphicsScene* m_scene;
    QGraphicsView* m_view;
    QImage m_image;
    QGraphicsItemGroup* m_currentItemGroup;  // 用于管理当前选中的图形组合(如矩形、卡尺等)
    QMenu* m_rightClickMenu;
    double m_scaleFactor;  // 缩放因子

    // 记录旋转矩形相关状态
    bool m_isDrawingRotatedRect;
    QPointF m_rotatedRectStartPoint;
    QPointF m_rotatedRectLastPoint;
    double m_rotatedRectRotation;

    // 正在操作的矩形角索引,用于四个角拖拽, -1 表示无操作
    int m_activeCornerIndex;
    // 记录矩形中心初始位置,用于中心平移
    QPointF m_rectCenterInitialPos;

    // 绘制矩形相关函数
    void drawRectangle(QPointF startPoint, QPointF endPoint);
    // 处理右键菜单事件
    void handleRightClickMenu(QMouseEvent* event);
    // 放大图片
    void zoomIn();
    // 缩小图片
    void zoomOut();
    // 平移图片
    void translateView(QPointF offset);

    // 找线卡尺工具相关函数
    void startLineMeasurement(QPointF startPoint);
    // 继续线测量
    void continueLineMeasurement(QPointF currentPoint);
    // 完成线测量
    void finishLineMeasurement(QPointF endPoint);

    // 圆形卡尺工具相关函数
    void startCircleMeasurement(QPointF centerPoint);
    // 继续圆测量
    void continueCircleMeasurement(QPointF currentPoint);
    // 完成圆测量
    void finishCircleMeasurement(QPointF currentPoint);

    // 旋转矩形工具相关函数
    void startRotatedRectMeasurement(QPointF startPoint);
    // 继续旋转矩形测量
    void continueRotatedRectMeasurement(QPointF currentPoint);
    // 完成旋转矩形测量
    void finishRotatedRectMeasurement(QPointF currentPoint);

    // 辅助函数,判断鼠标点击位置是否靠近矩形角
    bool isNearCorner(QPointF point, QRectF rect, double tolerance = 5.0);
    // 辅助函数,更新矩形旋转状态
    void updateRectRotation(QPointF currentPoint);
    // 辅助函数,根据鼠标移动更新矩形位置和大小
    void updateRectGeometry(QPointF currentPoint);
    // 辅助函数,显示矩形提示信息
    void showRectTooltip(QPointF point, QRectF rect);
};

#endif // IMAGEVIEWERWIDGET_H





// imageviewerwidget.cpp
#include "imageviewerWidget.h"

ImageViewerWidget::ImageViewerWidget(QWidget *parent) :
    QWidget(parent),
    m_scaleFactor(1.0),
    m_isDrawingRotatedRect(false),
    m_rotatedRectRotation(0),
    m_activeLineEndpointIndex(-1),
    m_lineCenterInitialPos(),
    m_activeCornerIndex(-1),
    m_rectCenterInitialPos()
{
    m_scene = new QGraphicsScene(this);
    m_view = new QGraphicsView(m_scene);
    m_view->setAlignment(Qt::AlignCenter);
    m_view->setDragMode(QGraphicsView::ScrollHandDrag);  // 开启平移模式

    m_rightClickMenu = new QMenu(this);
    m_currentItemGroup = nullptr;

    QVBoxLayout* layout = new QVBoxLayout(this);
    layout->addWidget(m_view);
    setLayout(layout);
}

ImageViewerWidget::~ImageViewerWidget()
{
    delete m_scene;
    delete m_view;
    delete m_rightClickMenu;
}

void ImageViewerWidget::updateImage(unsigned char* imageData, int width, int height)
{
    m_image = QImage(imageData, width, height, QImage::Format_RGB888);
    m_scene->clear();
    m_scene->addPixmap(QPixmap::fromImage(m_image));
    m_view->resetTransform();  // 重置视图变换,避免缩放等影响新图像显示
    m_scaleFactor = 1.0;
    m_isDrawingRotatedRect = false;
}

void ImageViewerWidget::addToolAction(QAction* action)
{
    m_rightClickMenu->addAction(action);
}

void ImageViewerWidget::removeToolAction(QAction* action)
{
    m_rightClickMenu->removeAction(action);
}

void ImageViewerWidget::mousePressEvent(QMouseEvent* event)
{
    if (event->button() == Qt::RightButton) {
        handleRightClickMenu(event);
    } else if (event->button() == Qt::LeftButton) {
        if (!m_currentItemGroup) {
            m_currentItemGroup = new QGraphicsItemGroup;
            m_scene->addItem(m_currentItemGroup);
        }
        if (m_rightClickMenu->actions().contains(findChild<QAction*>("lineMeasurementAction"))) {
            startLineMeasurement(mapToScene(event->pos()));
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("circleMeasurementAction"))) {
            startCircleMeasurement(mapToScene(event->pos()));
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("rotatedRectAction"))) {
            startRotatedRectMeasurement(mapToScene(event->pos()));
        } else {
            drawRectangle(mapToScene(event->pos()), mapToScene(event->pos()));
        }

        // 检查是否点击在线端点、旋转矩形角上
        if (m_currentItemGroup) {
            if (m_rightClickMenu->actions().contains(findChild<QAction*>("lineMeasurementAction"))) {
                QPainterPath linePath = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front())->path();
                m_activeLineEndpointIndex = getLineEndpointIndex(mapToScene(event->pos()), linePath);
                if (m_activeLineEndpointIndex!= -1) {
                    m_lineCenterInitialPos = linePath.boundingRect().center();
                }
            }
            if (m_rightClickMenu->actions().contains(findChild<QAction*>("rotatedRectAction"))) {
                QRectF rect = m_currentItemGroup->boundingRect();
                m_activeCornerIndex = getCornerIndex(mapToScene(event->pos()), rect);
                if (m_activeCornerIndex!= -1) {
                    m_rectCenterInitialPos = rect.center();
                }
            }
        }
    }
    QWidget::mousePressEvent(event);
}

void ImageViewerWidget::mouseMoveEvent(QMouseEvent* event)
{
    if (m_currentItemGroup && event->buttons() == Qt::LeftButton) {
        QPointF newPos = mapToScene(event->pos());
        QPointF offset = newPos - m_currentItemGroup->pos();
        m_currentItemGroup->moveBy(offset.x(), offset.y());
        if (m_rightClickMenu->actions().contains(findChild<QAction*>("lineMeasurementAction"))) {
            continueLineMeasurement(newPos);
            if (m_activeLineEndpointIndex!= -1) {
                updateLineGeometry(newPos);
            } else {
                updateLineRotation(newPos);
            }
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("circleMeasurementAction"))) {
            continueCircleMeasurement(newPos);
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("rotatedRectAction"))) {
            continueRotatedRectMeasurement(newPos);
            if (m_activeCornerIndex!= -1) {
                updateRectGeometry(newPos);
            } else {
                updateRectRotation(newPos);
            }
        }
        // 显示线、矩形或圆的提示信息
        if (m_rightClickMenu->actions().contains(findChild<QAction*>("lineMeasurementAction"))) {
            QPainterPath linePath = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front())->path();
            showLineTooltip(mapToScene(event->pos()), linePath);
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("rotatedRectAction"))) {
            QRectF rect = m_currentItemGroup->boundingRect();
            showRectTooltip(mapToScene(event->pos()), rect);
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("circleMeasurementAction"))) {
            QGraphicsPathItem* circleItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
            QPainterPath circlePath = circleItem->path();
            showCircleTooltip(mapToScene(event->pos()), circlePath);
        }
    }
    QWidget::mouseMoveEvent(event);
}

void ImageViewerWidget::mouseReleaseEvent(QMouseEvent* event)
{
    if (m_currentItemGroup && event->button() == Qt::LeftButton) {
        if (m_rightClickMenu->actions().contains(findChild<QAction*>("lineMeasurementAction"))) {
            finishLineMeasurement(mapToScene(event->pos()));
            m_activeLineEndpointIndex = -1;
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("circleMeasurementAction"))) {
            finishCircleMeasurement(mapToScene(event->pos()));
        } else if (m_rightClickMenu->actions().contains(findChild<QAction*>("rotatedRectAction"))) {
            finishRotatedRectMeasurement(mapToScene(event->pos()));
            m_activeCornerIndex = -1;
        } else {
            QRectF rect = m_currentItemGroup->boundingRect();
            emit shapeDrawn(rect);
        }
        m_currentItemGroup = nullptr;
    }
    QWidget::mouseReleaseEvent(event);
}

void ImageViewerWidget::wheelEvent(QWheelEvent* event)
{
    if (event->angleDelta().y() > 0) {
        zoomIn();
    } else {
        zoomOut();
    }
    QWidget::wheelEvent(event);
}

void ImageViewerWidget::drawRectangle(QPointF startPoint, QPointF endPoint)
{
    QGraphicsRectItem* rectItem = new QGraphicsRectItem(QRectF(startPoint, endPoint));
    m_currentItemGroup->addToGroup(rectItem);
}

void ImageViewerWidget::handleRightClickMenu(QMouseEvent* event)
{
    m_rightClickMenu->exec(mapToGlobal(event->pos()));
}

void ImageViewerWidget::zoomIn()
{
    m_scaleFactor *= 1.2;
    m_view->scale(m_scaleFactor, m_scaleFactor);
}

void ImageViewerWidget::zoomOut()
{
    m_scaleFactor /= 1.2;
    m_view->scale(m_scaleFactor, m_scaleFactor);
}

void ImageViewerWidget::translateView(QPointF offset)
{
    m_view->translate(offset.x(), offset.y());
}

void ImageViewerWidget::startLineMeasurement(QPointF startPoint)
{
    QGraphicsPathItem* lineItem = new QGraphicsPathItem;
    QPainterPath path;
    path.moveTo(startPoint);
    lineItem->setPath(path);
    m_currentItemGroup->addToGroup(lineItem);
}

void ImageViewerWidget::continueLineMeasurement(QPointF currentPoint)
{
    QGraphicsPathItem* lineItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
    QPainterPath path = lineItem->path();
    path.lineTo(currentPoint);
    lineItem->setPath(path);
}

void ImageViewerWidget::finishLineMeasurement(QPointF endPoint)
{
    QGraphicsPathItem* lineItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
    QPainterPath path = lineItem->path();
    path.lineTo(endPoint);
    lineItem->setPath(path);
    emit lineMeasured(path.elementAt(0).x, path.elementAt(0).y, path.elementAt(path.elementCount() - 1).x, path.elementAt(path.elementCount() - 1).y);
}

void ImageViewerWidget::updateLineGeometry(QPointF currentPoint)
{
    if (m_currentItemGroup) {
        QPainterPath linePath = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front())->path();
        QPointF center = linePath.boundingRect().center();
        QPointF offset = currentPoint - m_lineCenterInitialPos;
        switch (m_activeLineEndpointIndex) {
        case 0: // 起点
            linePath.setElementPositionAt(0, linePath.elementAt(0).x + offset.x(), linePath.elementAt(0).y + offset.y());
            break;
        case 1: // 终点
            linePath.setElementPositionAt(linePath.elementCount() - 1, linePath.elementAt(linePath.elementCount() - 1).x + offset.x(), linePath.elementAt(linePath.elementCount() - 1).y + offset.y());
            break;
        }
        QGraphicsPathItem* lineItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
        lineItem->setPath(linePath);
    }
}

bool ImageViewerWidget::isNearLineEndpoint(QPointF point, QPainterPath linePath, double tolerance = 5.0)
{
    const QPointF endpoints[2] = { QPointF(linePath.elementAt(0).x, linePath.elementAt(0).y), QPointF(linePath.elementAt(linePath.elementCount() - 1).x, linePath.elementAt(linePath.elementCount() - 1).y) };
    for (int i = 0; i < 2; i++) {
        if (QLineF(point, endpoints[i]).length() < tolerance) {
            return true;
        }
    }
    return false;
}

int ImageViewerWidget::getLineEndpointIndex(QPointF point, QPainterPath linePath)
{
    const QPointF endpoints[2] = { QPointF(linePath.elementAt(0).x, linePath.elementAt(0).y), QPointF(linePath.elementAt(linePath.elementCount() - 1).x, linePath.elementAt(linePath.elementCount() - 1).y) };
    for (int i = 0; i < 2; i++) {
        if (QLineF(point, endpoints[i]).length() < 5.0) {
            return i;
        }
    }
    return -1;
}

void ImageViewerWidget::updateLineRotation(QPointF currentPoint)
{
    if (m_currentItemGroup) {
        QPainterPath linePath = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front())->path();
        double angle = std::atan2(currentPoint.y() - linePath.boundingRect().center().y(), currentPoint.x() - linePath.boundingRect().center().x());
        QGraphicsPathItem* lineItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
        lineItem->setRotation(angle * 180 / M_PI);
    }
}

void ImageViewerWidget::startCircleMeasurement(QPointF centerPoint)
{
    QGraphicsPathItem* circleItem = new QGraphicsPathItem;
    QPainterPath path;
    path.addEllipse(centerPoint, 0, 0);
    circleItem->setPath(path);
    m_currentItemGroup->addToGroup(circleItem);
}

void ImageViewerWidget::continueCircleMeasurement(QPointF currentPoint)
{
    QGraphicsPathItem* circleItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
    QPainterPath path = circleItem->path();
    double radius = std::sqrt(std::pow(currentPoint.x() - path.elementAt(0).x, 2) + std::pow(currentPoint.y() - path.elementAt(0).y, 2));
    path = QPainterPath();
    path.addEllipse(path.elementAt(0).x, path.elementAt(0).y, radius, radius);
    circleItem->setPath(path);
}

void ImageViewerWidget::finishCircleMeasurement(QPointF currentPoint)
{
    QGraphicsPathItem* circleItem = static_cast<QGraphicsPathItem*>(m_currentItemGroup->childItems().front());
    QPainterPath path = circleItem->path();
    double radius = std::sqrt(std::pow(currentPoint.x() - path.elementAt(0).x, 2) + std::pow(currentPoint.y() - path.elementAt(0).y, 2));
    emit circleMeasured(path.elementAt(0).x, path.elementAt(0).y, radius);
}

void ImageViewerWidget::startRotatedRectMeasurement(QPointF startPoint)
{
    m_isDrawingRotatedRect = true;
    m_rotatedRectStartPoint = startPoint;
    m_rotatedRectLastPoint = startPoint;
}

void ImageViewerWidget::continueRotatedRectMeasurement(QPointF currentPoint)
{
    if (m_isDrawingRotatedRect) {
        QGraphicsItemGroup* group = m_currentItemGroup;

        // 先移除之前可能存在的矩形,用于实时更新显示
        QList<QGraphicsItem*> items = group->childItems();
        for (QGraphicsItem* item : items) {
            if (QGraphicsRectItem* rectItem = qgraphicsitem_cast<QGraphicsRectItem*>(item)) {
                group->removeFromGroup(rectItem);
                delete rectItem;
            }
        }

        QRectF rect = QRectF(m_rotatedRectStartPoint, currentPoint).normalized();
        double angle = std::atan2(currentPoint.y() - m_rotatedRectStartPoint.y(), currentPoint.x() - m_rotatedRectStartPoint.x());

        QGraphicsRectItem* rectItem = new QGraphicsRectItem(rect);
        rectItem->setTransformOriginPoint(rect.center());
        rectItem->setRotation(angle * 180 / M_PI);
        group->addToGroup(rectItem);

        m_rotatedRectLastPoint = currentPoint;
        m_rotatedRectRotation = angle;
    }
}

void ImageViewerWidget::finishRotatedRectMeasurement(QPointF currentPoint)
{
    if (m_isDrawingRotatedRect) {
        continueRotatedRectMeasurement(currentPoint);
        QRectF rect = QRectF(m_rotatedRectStartPoint, currentPoint).normalized();
        emit rotatedRectDrawn(rect, m_rotatedRectRotation * 180 / M_PI);
        m_isDrawingRotatedRect = false;
    }
}

bool ImageViewerWidget::isNearCorner(QPointF point, QRectF rect, double tolerance = 5.0)
{
    const QPointF corners[4] = { rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft() };
    for (int i = 0; i < 4; i++) {
        if (QLineF(point, corners[i]).length() < tolerance) {
            return true;
        }
    }
    return false;
}
int ImageViewerWidget::getCornerIndex(QPointF point, QRectF rect)
{
    const QPointF corners[4] = { rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft() };
    for (int i = 0; i < 4; i++) {
        if (QLineF(point, corners[i]).length() < 5.0) {
            return i;
        }
    }
    return -1;
}

void ImageViewerWidget::updateRectGeometry(QPointF currentPoint)
{
    if (m_currentItemGroup) {
        QRectF rect = m_currentItemGroup->boundingRect();
        QPointF center = rect.center();
        QPointF offset = currentPoint - m_rectCenterInitialPos;
        switch (m_activeCornerIndex) {
        case 0: // 左上角
            rect.setTopLeft(rect.topLeft() + offset);
            break;
        case 1: // 右上角
            rect.setTopRight(rect.topRight() + offset);
            break;
        case 2: // 右下角
            rect.setBottomRight(rect.bottomRight() + offset);
            break;
        case 3: // 左下角
            rect.setBottomLeft(rect.bottomLeft() + offset);
            break;
        }
        QGraphicsRectItem* rectItem = static_cast<QGraphicsRectItem*>(m_currentItemGroup->childItems().first());
        rectItem->setRect(rect);
    }
}

void ImageViewerWidget::updateRectRotation(QPointF currentPoint)
{
    if (m_currentItemGroup) {
        QRectF rect = m_currentItemGroup->boundingRect();
        double angle = std::atan2(currentPoint.y() - rect.center().y(), currentPoint.x() - rect.center().x());
        QGraphicsRectItem* rectItem = static_cast<QGraphicsRectItem*>(m_currentItemGroup->childItems().first());
        rectItem->setRotation(angle * 180 / M_PI);
        m_rotatedRectRotation = angle;
    }
}

void ImageViewerWidget::showLineTooltip(QPointF point, QPainterPath linePath)
{
    QPointF start = QPointF(linePath.elementAt(0).x, linePath.elementAt(0).y);
    QPointF end = QPointF(linePath.elementAt(linePath.elementCount() - 1).x, linePath.elementAt(linePath.elementCount() - 1).y);
    QString tooltipText = QString("线信息:\n起点坐标: (%1, %2)\n终点坐标: (%3, %4)")
                         .arg(start.x())
                         .arg(start.y())
                         .arg(end.x())
                         .arg(end.y());
    QToolTip::showText(mapToGlobal(mapFromScene(point)), tooltipText, this);
}

void ImageViewerWidget::showRectTooltip(QPointF point, QRectF rect)
{
    QString tooltipText = QString("矩形信息:\n左上角坐标: (%1, %2)\n右下角坐标: (%3, %4)\n旋转角度: %5°")
                         .arg(rect.topLeft().x())
                         .arg(rect.topLeft().y())
                         .arg(rect.bottomRight().x())
                         .arg(rect.bottomRight().y())
                         .arg(m_rotatedRectRotation * 180 / M_PI);
    QToolTip::showText(mapToGlobal(mapFromScene(point)), tooltipText, this);
}

void ImageViewerWidget::showCircleTooltip(QPointF point, QPainterPath circlePath)
{
    QPointF center = QPointF(circlePath.elementAt(0).x, circlePath.elementAt(0).y);
    double radius = std::sqrt(std::pow(point.x() - center.x, 2) + std::pow(point.y() - center.y, 2));
    QString tooltipText = QString("圆信息:\n圆心坐标: (%1, %2)\n半径: %3")
                         .arg(center.x())
                         .arg(center.y())
                         .arg(radius);
    QToolTip::showText(mapToGlobal(mapFromScene(point)), tooltipText, this);
}

以下是对 ImageViewerWidget.cpp 中各主要函数功能的简单介绍:

构造函数 ImageViewerWidget

  • 功能:初始化 ImageViewerWidget 类相关的成员变量、创建图形场景 m_scene、图形视图 m_view,设置视图平移模式等,还初始化右键菜单和用于管理图形组合的指针,搭建好界面布局,为后续操作做准备。

析构函数 ~ImageViewerWidget

  • 功能:释放之前构造函数中创建的 m_scenem_viewm_rightClickMenu 等资源,避免内存泄漏,进行清理工作。

updateImage

  • 功能:依据传入的图像数据更新显示的图像,包括清除原有场景内容、添加新图像、重置视图变换以及相关操作状态变量,确保后续操作基于新图像开展。

addToolAction

  • 功能:往右键菜单里添加一个工具操作选项(QAction),可动态扩充右键菜单功能。

removeToolAction

  • 功能:从右键菜单中移除指定的工具操作选项(QAction),实现动态调整菜单内容。

mousePressEvent

  • 功能:处理鼠标按下事件,右键按下弹出右键菜单,左键按下依据右键菜单中的工具选项启动相应图形绘制或测量操作,同时判断是否点击在图形关键位置(如线端点、矩形角)并记录相关信息。

mouseMoveEvent

  • 功能:响应鼠标移动且左键按下的情况,实现图形的平移,根据不同激活工具继续对应图形的绘制或修改操作(如更新线、圆、矩形的相关参数),还实时显示鼠标悬停处图形的提示信息。

mouseReleaseEvent

  • 功能:处理鼠标释放事件,针对不同激活工具完成相应操作、重置操作状态,并在普通矩形绘制时发送绘制完成信号,结束当前操作。

wheelEvent

  • 功能:依据鼠标滚轮滚动方向,调用相应函数实现图片放大或缩小功能。

drawRectangle

  • 功能:创建矩形图形项并添加到当前图形管理组,用于在场景中绘制矩形。

handleRightClickMenu

  • 功能:在鼠标右键点击位置弹出右键菜单,方便选择操作。

zoomIn

  • 功能:增大图片的缩放因子,实现图片放大显示效果。

zoomOut

  • 功能:减小图片的缩放因子,让图片缩小显示。

translateView

  • 功能:按照传入的偏移量对视图进行平移,便于浏览图像不同位置。

startLineMeasurement

  • 功能:开启找线卡尺测量,创建初始线图形项并添加到管理组,准备绘制线。

continueLineMeasurement

  • 功能:在找线卡尺测量中,随着鼠标移动持续更新线的终点位置,动态绘制线。

finishLineMeasurement

  • 功能:结束找线卡尺测量,确定线的最终形态,并发送测量完成信号携带线的起止坐标信息。

updateLineGeometry

  • 功能:根据鼠标操作情况,更新找线卡尺绘制线的端点位置,实现线的拖拽变形。

isNearLineEndpoint

  • 功能:判断鼠标位置是否靠近线的端点,辅助后续操作判断。

getLineEndpointIndex

  • 功能:获取鼠标位置靠近的线端点索引,确定操作的具体端点。

updateLineRotation

  • 功能:依据鼠标位置改变找线卡尺绘制线的旋转角度,实现线的旋转操作。

startCircleMeasurement

  • 功能:启动圆形卡尺测量,创建初始圆形图形项添加到管理组,准备后续绘制圆。

continueCircleMeasurement

  • 功能:在圆形卡尺测量中,随着鼠标移动更新圆的半径,动态绘制圆。

finishCircleMeasurement

  • 功能:结束圆形卡尺测量,确定圆的最终形态,并发送测量完成信号携带圆心和半径信息。

startRotatedRectMeasurement

  • 功能:开启旋转矩形绘制操作,记录起始点等初始信息。

continueRotatedRectMeasurement

  • 功能:在旋转矩形绘制中,根据鼠标位置实时更新矩形形状和旋转角度,动态展示旋转矩形。

finishRotatedRectMeasurement

  • 功能:结束旋转矩形绘制,确定最终矩形形态,并发送绘制完成信号携带矩形及旋转角度信息。

isNearCorner

  • 功能:判断鼠标位置是否靠近矩形的角,辅助后续矩形角操作判断。

getCornerIndex

  • 功能:获取鼠标位置靠近的矩形角索引,确定操作的具体角。

updateRectGeometry

  • 功能:依据鼠标对矩形角的操作,更新旋转矩形的大小和位置,实现角的拖拽改变矩形形态。

updateRectRotation

  • 功能:根据鼠标位置更新旋转矩形的旋转角度,实现旋转操作。

showLineTooltip

  • 功能:在鼠标悬停在线上时,显示包含线相关参数(起止坐标)的提示信息。

showRectTooltip

  • 功能:当鼠标悬停在矩形上时,展示包含矩形关键参数(坐标、旋转角度)的提示信息。

showCircleTooltip

  • 功能:鼠标悬停在圆上时,显示包含圆的圆心和半径等参数的提示信息。

cameramanager.h 和 cameramanager.cpp(相机管理类,负责相机业务逻辑)

// cameramanager.h
#ifndef CAMERAMANAGER_H
#define CAMERAMANAGER_H

#include <QObject>
#include <QTimer>
#include <vector>
#include "camerainterface.h"

class CameraManager : public QObject
{
    Q_OBJECT
public:
    CameraManager(QObject *parent = nullptr);
    ~CameraManager();

    // 初始化相机列表,根据实际连接的相机数量
    void initializeCameras();
    // 启动所有相机的图像采集
    void startCapture();
    // 停止所有相机的图像采集
    void stopCapture();
    // 手动触发所有相机采集一次图像
    void manualTrigger();
    // 关闭所有相机连接
    void closeAllCameras();

signals:
    // 图像采集成功信号,携带图像数据、宽度、高度以及相机索引
    void imageCaptured(unsigned char* imageData, int width, int height, int cameraIndex);
    // 相机出错信号,携带相机索引
    void cameraError(int cameraIndex);

private:
    std::vector<CameraInterface*> m_cameraWorkers;
    QTimer* m_reconnectionTimer;
    void setupCameraWorkers(int numCameras);
    void handleCameraError(int cameraIndex);
    void checkForReconnection();
};

// cameramanager.cpp
#include "cameramanager.h"

CameraManager::CameraManager(QObject *parent) :
    QObject(parent),
    m_reconnectionTimer(new QTimer(this))
{
    connect(m_reconnectionTimer, &QTimer::timeout, this, &CameraManager::checkForReconnection);
}

CameraManager::~CameraManager()
{
    closeAllCameras();
    delete m_reconnectionTimer;
    for (CameraInterface* worker : m_cameraWorkers) {
        delete worker;
    }
}

void CameraManager::initializeCameras()
{
    MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &m_deviceInfoList);
    int numCameras = m_deviceInfoList.nDeviceNum;
    setupCameraWorkers(numCameras);
}

void CameraManager::startCapture()
{
    for (CameraInterface* worker : m_cameraWorkers) {
        QThread* thread = new QThread;
        worker->moveToThread(thread);
        connect(thread, &QThread::started, worker, &CameraInterface::startCapture);
        connect(worker, &CameraInterface::destroyed, thread, &QThread::quit);
        connect(thread, &QThread::finished, thread, &QThread::deleteLater);
        thread->start();
    }
}

void CameraManager::stopCapture()
{
    for (CameraInterface* worker : m_cameraWorkers) {
        worker->stopCapture();
    }
}

void CameraManager::manualTrigger()
{
    for (CameraInterface* worker : m_cameraWorkers) {
        worker->manualTrigger();
    }
}

void CameraManager::closeAllCameras()
{
    for (CameraInterface* worker : m_cameraWorkers) {
        worker->disconnectCamera();
    }
}

void CameraManager::setupCameraWorkers(int numCameras)
{
    m_cameraWorkers.resize(numCameras);
    for (int i = 0; i < numCameras; ++i) {
        m_cameraWorkers[i] = new HikCameraWorker(i);
        connect(m_cameraWorkers[i], &CameraInterface::imageCaptured, this, [this, i](unsigned char* imageData, int width, int height) {
            emit imageCaptured(imageData, width, height, i);
        });
        connect(m_cameraWorkers[i], &CameraInterface::cameraError, this, &CameraManager::handleCameraError);
    }
}

void CameraManager::handleCameraError(int cameraIndex)
{
    m_cameraWorkers[cameraIndex]->stopCapture();
    m_cameraWorkers[cameraIndex]->disconnectCamera();
    m_reconnectionTimer->start(5000);
    emit cameraError(cameraIndex);
}

void CameraManager::checkForReconnection()
{
    m_reconnectionTimer->stop();
    for (int i = 0; i < m_cameraWorkers.size(); ++i) {
        if (!m_cameraWorkers[i]->isConnected()) {
            QThread* thread = new QThread;
            m_cameraWorkers[i]->moveToThread(thread);
            connect(thread, &QThread::started, m_cameraWorkers[i], &CameraInterface::startCapture);
            connect(m_cameraWorkers[i], &CameraInterface::destroyed, thread, &QThread::quit);
            connect(thread, &QThread::finished, thread, &QThread::deleteLater);
            thread->start();
        }
    }
}

mainwindow.h 和 mainwindow.cpp(主窗口类)

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QTextEdit>
#include <vector>
#include "imageviewerwidget.h"

class CameraManager;

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void onStartCaptureClicked();
    void onStopCaptureClicked();
    void updateImage(unsigned char* imageData, int width, int height, int cameraIndex);
    void handleCameraError(int cameraIndex);
    void onManualTriggerClicked();
    void onOpenCameraClicked();
    void onCloseCameraClicked();
    void onToggleCameraClicked();

private:
    CameraManager* m_cameraManager;
    std::vector<ImageViewerWidget*> m_imageViewers;
    QMenuBar* m_menuBar;
    QToolBar* m_toolBar;
    QStatusBar* m_statusBar;
    QAction* m_startCaptureAction;
    QAction* m_stopCaptureAction;
    QAction* m_manualTriggerAction;
    QAction* m_openCameraAction;
    QAction* m_closeCameraAction;
    QAction* m_toggleCameraAction;
    QWidget* m_centralWidget;
    QVBoxLayout* m_layout;
    QHBoxLayout* m_imageLayout;
    QTextEdit* m_logTextEdit;

    void setupMenuBar();
    void setupToolBar();
    void setupStatusBar();
    void setupCameraUI();
    void addImageViewerWidget();
    void removeImageViewerWidget();
};

#endif // MAINWINDOW_H
#endif // MAINWINDOW_H


// mainwindow.cpp
#include "mainwindow.h"
#include "cameramanager.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    m_cameraManager = new CameraManager();
    m_cameraManager->initializeCameras();

    setupCameraUI();

    connect(m_cameraManager, &CameraManager::imageCaptured, this, &MainWindow::updateImage);
    connect(m_cameraManager, &CameraManager::cameraError, this, &MainWindow::handleCameraError);
}

MainWindow::~MainWindow()
{
    delete m_cameraManager;
    for (ImageViewerWidget* viewer : m_imageViewers) {
        delete viewer;
    }
}

void MainWindow::setupMenuBar()
{
    m_menuBar = new QMenuBar(this);

    QMenu* cameraMenu = m_menuBar->addMenu("相机");
    m_startCaptureAction = cameraMenu->addAction("开始采集");
    connect(m_startCaptureAction, &QAction::triggered, this, &MainWindow::onStartCaptureClicked);
    m_stopCaptureAction = cameraMenu->addAction("停止采集");
    connect(m_stopCaptureAction, &QAction::triggered, this, &MainWindow::onStopCaptureClicked);
    m_manualTriggerAction = cameraMenu->addAction("手动触发");
    connect(m_manualTriggerAction, &QAction::triggered, this, &MainWindow::onManualTriggerClicked);
    m_openCameraAction = cameraMenu->addAction("打开相机");
    connect(m_openCameraAction, &QAction::triggered, this, &MainWindow::onOpenCameraClicked);
    m_closeCameraAction = cameraMenu->addAction("关闭相机");
    connect(m_closeCameraAction, &QAction::triggered, this, &MainWindow::onCloseCameraClicked);
    m_toggleCameraAction = cameraMenu->addAction("动态开关相机");
    connect(m_toggleCameraAction, &QAction::triggered, this, &MainWindow::onToggleCameraClicked);

    setMenuBar(m_menuBar);
}

void MainWindow::setupToolBar()
{
    m_toolBar = new QToolBar(this);
    m_toolBar->addAction(m_startCaptureAction);
    m_toolBar->addAction(m_stopCaptureAction);
    m_toolBar->addAction(m_manualTriggerAction);
    m_toolBar->addAction(m_openCameraAction);
    m_toolBar->addAction(m_closeCameraAction);
    m_toolBar->addAction(m_toggleCameraAction);
    addToolBar(m_toolBar);
}

void MainWindow::setupStatusBar()
{
    m_statusBar = new QStatusBar(this);
    setStatusBar(m_statusBar);
}

void MainWindow::setupCameraUI()
{
    int numCameras = m_cameraManager->getCameraCount();

    m_imageViewers.resize(numCameras);

    m_centralWidget = new QWidget(this);
    setCentralWidget(m_centralWidget);

    m_layout = new QVBoxLayout(m_centralWidget);

    m_imageLayout = new QHBoxLayout();
    m_layout->addLayout(m_imageLayout);

    m_logTextEdit = new QTextEdit(this);
    m_layout->addWidget(m_logTextEdit);

    for (int i = 0; i < numCameras; ++i) {
        m_imageViewers[i] = new ImageViewerWidget(this);
        m_imageLayout->addWidget(m_imageViewers[i]);
    }

    setupMenuBar();
    setupToolBar();
    setupStatusBar();
}

void MainWindow::onStartCaptureClicked()
{
    m_cameraManager->startCapture();
}

void MainWindow::onStopCaptureClicked()
{
    m_cameraManager->stopCapture();
}

void MainWindow::updateImage(unsigned char* imageData, int width, int height, int cameraIndex)
{
    if ( cameraIndex >= 0 && cameraIndex < m_imageViewers.size()) {
        m_imageViewers[cameraIndex]->updateImage(imageData, width, height);
    }
}

void MainWindow::handleCameraError(int cameraIndex)
{
    m_logTextEdit->append("相机 " + QString::number(cameraIndex) + " 出错");
}

void MainWindow::onManualTriggerClicked()
{
    m_cameraManager->manualTrigger();
}

void MainWindow::onOpenCameraClicked()
{
    m_logTextEdit->append("打开相机操作被触发");
}

void MainWindow::onCloseCameraClicked()
{
    m_cameraManager->closeAllCameras();
    m_logTextEdit->append("关闭相机操作被触发");
}

void MainWindow::onToggleCameraClicked()
{
    if (m_imageViewers.size() < m_cameraManager->getCameraCount()) {
        addImageViewerWidget();
    } else if (m_imageViewers.size() > 0) {
        removeImageViewerWidget();
    }
}

void MainWindow::addImageViewerWidget()
{
    int newIndex = m_imageViewers.size();
    ImageViewerWidget* newWidget = new ImageViewerWidget(this);
    m_imageViewers.push_back(newWidget);
    m_imageLayout->addWidget(newWidget);
}

void MainWindow::removeImageViewerWidget()
{
    if (!m_imageViewers.empty()) {
        int lastIndex = m_imageViewers.size() - 1;
        ImageViewerWidget* widgetToRemove = m_imageViewers.back();
        m_imageViewers.pop_back();
        m_imageLayout->removeWidget(widgetToRemove);
        delete widgetToRemove;
    }
}

在上述代码中:

  • 新增了一个 QAction 用于表示动态开关相机按钮,以及对应的槽函数 onToggleCameraClicked。
  • 在 onToggleCameraClicked 函数中,根据当前显示图片控件数量与相机总数的比较,决定是添加还是移除一个 ImageViewerWidget。
    addImageViewerWidget 函数用于创建并添加新的图片显示控件到布局中,removeImageViewerWidget 函数用于移除最后一个图片显示控件并释放相关资源。
  • 通过这些修改,实现了动态调整显示图片控件的功能,以适配相机的开启与关闭操作。
    请注意,上述代码基于之前提供的代码框架基础上修改,运行时需要确保 CameraManager 类及其他相关依赖正确实现并链接。

main.cpp(程序入口)

#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    MainWindow window;
    window.show();

    return app.exec();
}

4. 功能说明

  • 多线程取图:每个 HikCameraWorker 运行在独立线程中,通过 startCapture 启动相机采集,在 imageCallback 回调中发送采集到的图像信号,避免阻塞主线程,保证界面响应流畅。
  • 断线重连:当相机出现错误(如连接断开),触发 cameraError 信号,在 handleCameraError 槽函数中停止当前相机取图

http://www.kler.cn/a/450150.html

相关文章:

  • 金仓数据库安装-Kingbase v9-centos
  • 定位方式:css
  • 【QSS样式表 - ⑥】:QPushButton控件样式
  • Leetcode-208. 实现Trie(前缀树)
  • Golang学习历程【第三篇 基本数据类型类型转换】
  • Redis分片集群学习总结
  • Spring Security 自动踢掉前一个登录用户,一个配置搞定!,网易前端社招面经
  • 前端框架Vue的路由机制
  • 【已解决】黑马点评项目Redis版本替换过程中误删数据库后前端显示出现的问题
  • 基于 SOME/IP 的动态服务发现与调用:原理、实现与示例全解析
  • selenium学习笔记(一)
  • 软件测试之非功能测试设计
  • 自然语言编写的prompt为啥比不上编程语言prompt高效?
  • LeetCode 209. 长度最小的子数组 (C++实现)
  • 编译libtorch时报错:NvToolsExt Could not open input file ***nvToolsExt64_1.lib
  • javaScript中slice()和splice()的用法与区别
  • 重温设计模式--职责链模式
  • Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
  • CS!GO
  • 灰度测试是什么
  • 【NLP 17、NLP的基础——分词】
  • 用套接字的UDP,TCP知道什么是HTTP吗?
  • Apache解析漏洞(apache_parsing_vulnerability靶场攻略CVE-2017-15715靶场攻略)
  • 服务平滑发布与线上验证
  • CNN、RNN、LSTM和Transformer之间的区别和联系
  • 安装CPU版的torch(清华源)