From 2747d5f967f5f4370a094c4bdcff90ee960847dd Mon Sep 17 00:00:00 2001 From: tangchao0503 <735056338@qq.com> Date: Fri, 18 Sep 2026 14:31:28 +0800 Subject: [PATCH] =?UTF-8?q?add=20mapcavas=E6=B7=BB=E5=8A=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=9A=E9=95=9C=E5=83=8F=E5=92=8C=E6=97=8B=E8=BD=AC?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=9D=E5=AD=98=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HPPA/AppSettings.cpp | 33 +++++ HPPA/AppSettings.h | 15 ++ HPPA/HPPA.cpp | 31 ++++ HPPA/HPPA.qrc | 3 + HPPA/HPPA.ui | 24 +++ HPPA/ImageViewer.cpp | 305 ++++++++++++++++++++++++++++++++++----- HPPA/ImageViewer.h | 25 ++++ HPPA/MapToolSpectral.cpp | 28 +++- 8 files changed, 422 insertions(+), 42 deletions(-) diff --git a/HPPA/AppSettings.cpp b/HPPA/AppSettings.cpp index d864698..c0933e4 100644 --- a/HPPA/AppSettings.cpp +++ b/HPPA/AppSettings.cpp @@ -12,6 +12,9 @@ const double AppSettings::kDefaultScanSpeed = 1.0; const double AppSettings::kDefaultReturnSpeed = 5.0; const int AppSettings::kDefaultGonggaShanRecordPort = 666; const AppSettings::HyperimgDisplayMode AppSettings::kDefaultHyperimgDisplayMode = HyperimgDisplayMode::Waterfall; +const bool AppSettings::kDefaultImageHorizontalMirror = true; +const bool AppSettings::kDefaultImageVerticalMirror = true; +const int AppSettings::kDefaultImageRotation = 0; AppSettings::AppSettings() : m_settings(QSettings::IniFormat, QSettings::UserScope, @@ -216,3 +219,33 @@ void AppSettings::setHyperimgDisplayMode(HyperimgDisplayMode mode) { m_settings.setValue("Display/HyperimgDisplayMode", static_cast(mode)); } + +bool AppSettings::imageHorizontalMirror() const +{ + return m_settings.value("Display/ImageHorizontalMirror", kDefaultImageHorizontalMirror).toBool(); +} + +void AppSettings::setImageHorizontalMirror(bool value) +{ + m_settings.setValue("Display/ImageHorizontalMirror", value); +} + +bool AppSettings::imageVerticalMirror() const +{ + return m_settings.value("Display/ImageVerticalMirror", kDefaultImageVerticalMirror).toBool(); +} + +void AppSettings::setImageVerticalMirror(bool value) +{ + m_settings.setValue("Display/ImageVerticalMirror", value); +} + +int AppSettings::imageRotation() const +{ + return m_settings.value("Display/ImageRotation", kDefaultImageRotation).toInt(); +} + +void AppSettings::setImageRotation(int value) +{ + m_settings.setValue("Display/ImageRotation", ((value % 360) + 360) % 360); +} diff --git a/HPPA/AppSettings.h b/HPPA/AppSettings.h index 5a3b2a2..af79a79 100644 --- a/HPPA/AppSettings.h +++ b/HPPA/AppSettings.h @@ -73,6 +73,18 @@ public: // 图像显示模式 HyperimgDisplayMode hyperimgDisplayMode() const; void setHyperimgDisplayMode(HyperimgDisplayMode mode); + + // 图像显示变换(左右镜像/上下镜像/旋转),全局生效 + bool imageHorizontalMirror() const; + void setImageHorizontalMirror(bool value); + + bool imageVerticalMirror() const; + void setImageVerticalMirror(bool value); + + // 旋转角度,取值 0、90、180、270 + int imageRotation() const; + void setImageRotation(int value); + // 在此处添加更多参数的 getter/setter ... private: @@ -94,4 +106,7 @@ private: static const double kDefaultReturnSpeed; static const int kDefaultGonggaShanRecordPort; static const HyperimgDisplayMode kDefaultHyperimgDisplayMode; + static const bool kDefaultImageHorizontalMirror; + static const bool kDefaultImageVerticalMirror; + static const int kDefaultImageRotation; }; diff --git a/HPPA/HPPA.cpp b/HPPA/HPPA.cpp index 9fdbee1..3a65afc 100644 --- a/HPPA/HPPA.cpp +++ b/HPPA/HPPA.cpp @@ -959,6 +959,9 @@ void HPPA::initMenubarToolbar() ui.mActionPan->setIcon(QIcon(":/svg/resources/icons/svg/pan.svg")); ui.mActionSpectral->setIcon(QIcon(":/svg/resources/icons/svg/spectral.svg")); + ui.mActionRotation90->setIcon(QIcon(":/svg/resources/icons/svg/rotate90.svg")); + ui.mActionVerticalMirror->setIcon(QIcon(":/svg/resources/icons/svg/VerticalMirror.svg")); + ui.mActionHorizontalMirror->setIcon(QIcon(":/svg/resources/icons/svg/HorizontalMirror.svg")); connect(ui.mActionPan, &QAction::toggled, this, [=](bool checked) { if (checked) @@ -974,6 +977,34 @@ void HPPA::initMenubarToolbar() ui.mActionSpectral->setIcon(QIcon(":/svg/resources/icons/svg/spectral.svg")); }); + connect(ui.mActionRotation90, &QAction::triggered, this, [=](bool) { + QWidget* currentWidget = m_imageViewerTabWidget->currentWidget(); + if (!currentWidget) return; + + QList canvases = currentWidget->findChildren(); + if (canvases.isEmpty()) return; + + canvases[0]->rotateImage90(); + }); + connect(ui.mActionVerticalMirror, &QAction::triggered, this, [=](bool) { + QWidget* currentWidget = m_imageViewerTabWidget->currentWidget(); + if (!currentWidget) return; + + QList canvases = currentWidget->findChildren(); + if (canvases.isEmpty()) return; + + canvases[0]->toggleVerticalMirror(); + }); + connect(ui.mActionHorizontalMirror, &QAction::triggered, this, [=](bool) { + QWidget* currentWidget = m_imageViewerTabWidget->currentWidget(); + if (!currentWidget) return; + + QList canvases = currentWidget->findChildren(); + if (canvases.isEmpty()) return; + + canvases[0]->toggleHorizontalMirror(); + }); + // 使用样式表设置透明背景 toolBar->setStyleSheet(R"( QToolBar { diff --git a/HPPA/HPPA.qrc b/HPPA/HPPA.qrc index d180d7a..2076ce2 100644 --- a/HPPA/HPPA.qrc +++ b/HPPA/HPPA.qrc @@ -32,6 +32,9 @@ resources/icons/svg/tree_tri_down.svg resources/icons/svg/tree_tri_right.svg resources/icons/svg/mIconRaster.svg + resources/icons/svg/HorizontalMirror.svg + resources/icons/svg/rotate90.svg + resources/icons/svg/VerticalMirror.svg resources/icons/png/Spectral_Insight_27.png diff --git a/HPPA/HPPA.ui b/HPPA/HPPA.ui index b556dbf..7478b19 100644 --- a/HPPA/HPPA.ui +++ b/HPPA/HPPA.ui @@ -203,6 +203,9 @@ QToolBar QToolButton:hover { + + + @@ -758,6 +761,27 @@ QPushButton:pressed 贡嘎山旋转平台 + + + 上下镜像 + + + + + 左右镜像 + + + 左右镜像 + + + + + 旋转90度 + + + 旋转90度 + + diff --git a/HPPA/ImageViewer.cpp b/HPPA/ImageViewer.cpp index 0fd88db..9aba323 100644 --- a/HPPA/ImageViewer.cpp +++ b/HPPA/ImageViewer.cpp @@ -49,7 +49,8 @@ Mapcavas::Mapcavas(QWidget* pParent) :QGraphicsView(pParent) setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFrameShape(QFrame::NoFrame); - m_displayMode = AppSettings::instance().hyperimgDisplayMode(); + loadSettings(); + //addSceneCoordinateSystem(); } Mapcavas::~Mapcavas() @@ -57,6 +58,94 @@ Mapcavas::~Mapcavas() } +void Mapcavas::loadSettings() +{ + updateDisplayMode(); + + //更新控制图像变换的变量m_imageTransformState + AppSettings& settings = AppSettings::instance(); + m_imageTransformState.horizontalMirror = settings.imageHorizontalMirror(); + m_imageTransformState.verticalMirror = settings.imageVerticalMirror(); + m_imageTransformState.rotation = settings.imageRotation(); +} + +void Mapcavas::applyImageTransform() +{ + if (m_GraphicsPixmapItemHandle == nullptr) + return; + + const double sx = m_imageTransformState.horizontalMirror ? -1.0 : 1.0; + const double sy = m_imageTransformState.verticalMirror ? -1.0 : 1.0; + const QPointF center = m_GraphicsPixmapItemHandle->boundingRect().center(); + + QTransform transform_all; + transform_all.translate(center.x(), center.y());// 围绕图片中心变换 + transform_all.scale(sx, sy); + transform_all.rotate(m_imageTransformState.rotation); + transform_all.translate(-center.x(), -center.y()); + + // setTransform 是替换而非叠加,因此可对已显示的图像重复调用 + m_GraphicsPixmapItemHandle->setTransform(transform_all); + + //QPointF itemOrigin(0, 0); + //QPointF sceneOrigin = m_GraphicsPixmapItemHandle->mapToScene(itemOrigin); + //QPointF sceneOrigin2 = m_GraphicsPixmapItemHandle->scenePos(); + + // 变换后重新定位十字叉,使其始终跟随同一个图像像素 + repositionCrosshair(); +} + +void Mapcavas::repositionCrosshair() +{ + if (!m_hLine || !m_vLine || !m_GraphicsPixmapItemHandle) + return; + + const QPointF scenePt = m_GraphicsPixmapItemHandle->mapToScene(m_crosshairItemPos); + m_hLine->setPos(scenePt); + m_vLine->setPos(scenePt); +} + +void Mapcavas::refreshImageTransform() +{ + if (!HasImage()) + return; + + applyImageTransform(); + + if (m_displayMode == AppSettings::HyperimgDisplayMode::Full) + { + ensureSceneVisible(); + } + else if (m_displayMode == AppSettings::HyperimgDisplayMode::Waterfall) + { + ensureWaterfallVisible(); + } +} + +void Mapcavas::rotateImage90() +{ + AppSettings& settings = AppSettings::instance(); + settings.setImageRotation(settings.imageRotation() + 90); + loadSettings(); + refreshImageTransform(); +} + +void Mapcavas::toggleHorizontalMirror() +{ + AppSettings& settings = AppSettings::instance(); + settings.setImageHorizontalMirror(!settings.imageHorizontalMirror()); + loadSettings(); + refreshImageTransform(); +} + +void Mapcavas::toggleVerticalMirror() +{ + AppSettings& settings = AppSettings::instance(); + settings.setImageVerticalMirror(!settings.imageVerticalMirror()); + loadSettings(); + refreshImageTransform(); +} + void Mapcavas::updateDisplayMode() { m_displayMode = AppSettings::instance().hyperimgDisplayMode(); @@ -79,14 +168,12 @@ void Mapcavas::SetImage(QPixmap *image) m_GraphicsPixmapItemHandle->setPixmap(*image); } - if (m_displayMode == AppSettings::HyperimgDisplayMode::Full) - { - ensureSceneVisible(); - } - else if (m_displayMode == AppSettings::HyperimgDisplayMode::Waterfall) - { - ensureWaterfallVisible(); - } + refreshImageTransform(); +} + +QGraphicsPixmapItem* Mapcavas::pixmapItemHandle() +{ + return m_GraphicsPixmapItemHandle; } void Mapcavas::ensureSceneVisible() @@ -108,40 +195,88 @@ void Mapcavas::ensureSceneVisible() void Mapcavas::ensureWaterfallVisible() { - resetTransform(); - - auto items = this->scene()->items(); - if (items.isEmpty()) + if (!HasImage()) return; - auto scene_rect = this->scene()->itemsBoundingRect(); + resetTransform(); + + QGraphicsPixmapItem* item = m_GraphicsPixmapItemHandle; + const QRectF item_rect = item->boundingRect(); + if (item_rect.isEmpty()) + return; + + // 图像已经应用了 m_imageTransformState 的镜像/旋转,这里以场景中的实际位置为准 + const QRectF scene_rect = item->sceneBoundingRect(); + qreal view_width = viewport()->rect().width(); qreal view_height = viewport()->rect().height(); + if (view_width <= 0.0 || view_height <= 0.0) + return; + if (scene_rect.width() <= 0.0 || scene_rect.height() <= 0.0) + return; - double x_scale = view_width / scene_rect.width(); - double y_scale = view_height / scene_rect.height(); + // 图像坐标中:x 为扫描线空间轴(固定),y 为采集帧数(不断增长),最新一帧位于 y 最大处。 + // 经过镜像/旋转后,增长轴可能竖直(0°/180°)或水平(90°/270°)。 + const QPointF growth_dir = item->mapToScene(QPointF(0.0, 1.0)) + - item->mapToScene(QPointF(0.0, 0.0)); + const bool growth_vertical = qAbs(growth_dir.y()) >= qAbs(growth_dir.x()); - double scale_factor = qMin(x_scale, y_scale) * 0.9; + // 最新一帧(图像底边中心)在场景中的位置,用于判断应该贴哪条边 + const QPointF newest_scene = item->mapToScene( + QPointF(item_rect.width() / 2.0, item_rect.height())); - scale(x_scale, x_scale); - m_scale *= x_scale; - - // 计算缩放后的图片可见高度 - qreal scaled_height = scene_rect.height() * x_scale; - - // 根据图片高度决定 Y 坐标 - qreal center_y; - if (scaled_height <= view_height) + if (growth_vertical) { - center_y = scene_rect.center().y(); + // 竖直瀑布:空间轴横向铺满视图宽度,最新一帧固定在上/下边缘 + const double x_scale = view_width / scene_rect.width(); + scale(x_scale, x_scale); + m_scale *= x_scale; + + // 计算缩放后的图片可见高度 + qreal scaled_height = scene_rect.height() * x_scale; + + qreal center_y; + if (scaled_height <= view_height) + { + center_y = scene_rect.center().y(); + } + else + { + qreal half_view_height = view_height / 2.0 / x_scale; + if (newest_scene.y() >= scene_rect.center().y()) + center_y = scene_rect.bottom() - half_view_height; // 最新帧在下方,贴底 + else + center_y = scene_rect.top() + half_view_height; // 最新帧在上方,贴顶 + } + + centerOn(scene_rect.center().x(), center_y); } else { - qreal half_view_height = view_height / 2.0 / x_scale; - center_y = scene_rect.bottom() - half_view_height; - } + // 水平瀑布:空间轴竖向铺满视图高度,最新一帧固定在左/右边缘 + const double y_scale = view_height / scene_rect.height(); + scale(y_scale, y_scale); + m_scale *= y_scale; - centerOn(scene_rect.center().x(), center_y); + // 计算缩放后的图片可见宽度 + qreal scaled_width = scene_rect.width() * y_scale; + + qreal center_x; + if (scaled_width <= view_width) + { + center_x = scene_rect.center().x(); + } + else + { + qreal half_view_width = view_width / 2.0 / y_scale; + if (newest_scene.x() >= scene_rect.center().x()) + center_x = scene_rect.right() - half_view_width; // 最新帧在右侧,贴右 + else + center_x = scene_rect.left() + half_view_width; // 最新帧在左侧,贴左 + } + + centerOn(center_x, scene_rect.center().y()); + } } bool Mapcavas::HasImage() @@ -164,21 +299,32 @@ void Mapcavas::updateCrosshair(double sceneX, double sceneY) if (!m_hLine) { m_hLine = m_qtGraphicsScene->addLine(0, 0, 0, 0, pen); + m_hLine->setFlag(QGraphicsItem::ItemIgnoresTransformations, true); m_hLine->setZValue(1e9); } if (!m_vLine) { m_vLine = m_qtGraphicsScene->addLine(0, 0, 0, 0, pen); + m_vLine->setFlag(QGraphicsItem::ItemIgnoresTransformations, true); m_vLine->setZValue(1e9); } m_hLine->setPen(pen); m_vLine->setPen(pen); - m_hLine->setLine(sceneX - m_CrosshairHalfLen, sceneY, - sceneX + m_CrosshairHalfLen, sceneY); - m_vLine->setLine(sceneX, sceneY - m_CrosshairHalfLen, - sceneX, sceneY + m_CrosshairHalfLen); + // 设置 ItemIgnoresTransformations 后,图元的原点依旧按场景坐标定位, + // 但局部坐标不再受视图缩放影响(1 单位 = 1 屏幕像素)。 + // 这里把锚点记录在图像(图元)坐标系中,再据此定位十字叉: + // 这样点击某个像素后,无论滚轮缩放还是镜像/旋转图像,十字叉都锚定在该像素上。 + const QPointF scenePt(sceneX + 0.5, sceneY + 0.5); + m_crosshairItemPos = m_GraphicsPixmapItemHandle->mapFromScene(scenePt); + + repositionCrosshair(); + + m_hLine->setLine(-m_CrosshairHalfLen, 0.0, + m_CrosshairHalfLen, 0.0); + m_vLine->setLine(0.0, -m_CrosshairHalfLen, + 0.0, m_CrosshairHalfLen); } void Mapcavas::removeCrosshair() @@ -197,6 +343,7 @@ void Mapcavas::removeCrosshair() delete m_vLine; m_vLine = nullptr; } + m_crosshairItemPos = QPointF(); } @@ -376,3 +523,91 @@ MapTool* Mapcavas::mapTool() const { return m_mapTool; } + +void Mapcavas::addSceneCoordinateSystem() +{ + const double length = 200.0; + const double arrowSize = 10.0; + + // 坐标系作为参考叠加层,需要始终绘制在图像和其他图元之上; + // 十字叉使用更大的 Z 值(1e9),因此这里取一个略小的值。 + const double zOrder = 1e8; + + // X 轴 + QPen xPen(Qt::red); + xPen.setWidth(2); + + QGraphicsLineItem* xAxis = m_qtGraphicsScene->addLine( + 0, 0, + length, 0, + xPen + ); + xAxis->setZValue(zOrder); + + // X 箭头 + QPolygonF xArrow; + xArrow << QPointF(length, 0) + << QPointF(length - arrowSize, -arrowSize / 2) + << QPointF(length - arrowSize, arrowSize / 2); + + QGraphicsPolygonItem* xArrowItem = m_qtGraphicsScene->addPolygon( + xArrow, + xPen, + QBrush(Qt::red) + ); + xArrowItem->setZValue(zOrder); + + + // Y 轴 + QPen yPen(Qt::green); + yPen.setWidth(2); + + QGraphicsLineItem* yAxis = m_qtGraphicsScene->addLine( + 0, 0, + 0, length, + yPen + ); + yAxis->setZValue(zOrder); + + // Y 箭头 + QPolygonF yArrow; + yArrow << QPointF(0, length) + << QPointF(-arrowSize / 2, length - arrowSize) + << QPointF(arrowSize / 2, length - arrowSize); + + QGraphicsPolygonItem* yArrowItem = m_qtGraphicsScene->addPolygon( + yArrow, + yPen, + QBrush(Qt::green) + ); + yArrowItem->setZValue(zOrder); + + + // 原点 + QGraphicsEllipseItem* originItem = m_qtGraphicsScene->addEllipse( + -4, -4, + 8, 8, + QPen(Qt::blue), + QBrush(Qt::blue) + ); + originItem->setZValue(zOrder); + + + // X 标签 + QGraphicsTextItem* xText = m_qtGraphicsScene->addText("X"); + xText->setDefaultTextColor(Qt::red); + xText->setPos(length + 5, -15); + xText->setZValue(zOrder); + + // Y 标签 + QGraphicsTextItem* yText = m_qtGraphicsScene->addText("Y"); + yText->setDefaultTextColor(Qt::green); + yText->setPos(5, length + 5); + yText->setZValue(zOrder); + + // 原点标签 + QGraphicsTextItem* originText = m_qtGraphicsScene->addText("(0, 0)"); + originText->setDefaultTextColor(Qt::blue); + originText->setPos(5, 5); + originText->setZValue(zOrder); +} diff --git a/HPPA/ImageViewer.h b/HPPA/ImageViewer.h index 6d96f97..777b39a 100644 --- a/HPPA/ImageViewer.h +++ b/HPPA/ImageViewer.h @@ -61,6 +61,16 @@ public: void unsetMapTool(MapTool* tool); MapTool* mapTool() const; + QGraphicsPixmapItem* pixmapItemHandle(); + + // 图像显示变换(左右镜像/上下镜像/旋转),状态保存在 AppSettings 中全局生效 + void rotateImage90(); + void toggleHorizontalMirror(); + void toggleVerticalMirror(); + + // 把当前的镜像/旋转重新应用到已经显示的图像,并按显示模式重新适配视图 + void refreshImageTransform(); + protected: QGraphicsScene *m_qtGraphicsScene; private: @@ -79,9 +89,24 @@ private: double m_CrosshairHalfLen = 10.0; QGraphicsLineItem* m_hLine = nullptr; // horizontal line QGraphicsLineItem* m_vLine = nullptr; // vertical line + QPointF m_crosshairItemPos; // 十字叉锚点,保存在图像(图元)坐标系 AppSettings::HyperimgDisplayMode m_displayMode; + struct ImageTransformState + { + bool horizontalMirror = true; + bool verticalMirror = true; + int rotation = 90*3; // 0、90、180、270 + }; + ImageTransformState m_imageTransformState; + + void applyImageTransform(); + void repositionCrosshair(); + void loadSettings(); + + void addSceneCoordinateSystem(); + signals: void leftMouseButtonPressed(int, int, QVector, QVector); }; diff --git a/HPPA/MapToolSpectral.cpp b/HPPA/MapToolSpectral.cpp index 42741d4..15f777d 100644 --- a/HPPA/MapToolSpectral.cpp +++ b/HPPA/MapToolSpectral.cpp @@ -37,21 +37,35 @@ void MapToolSpectral::canvasMousePressEvent(QMouseEvent* e) if (!canvas()) return; - const QPointF scenePt = canvas()->mapToScene(e->pos()); - const int x = static_cast(std::floor(scenePt.x())); - const int y = static_cast(std::floor(scenePt.y())); + //获取图像元素 + QGraphicsPixmapItem* item = canvas()->pixmapItemHandle(); + QPointF scenePos = item->pos(); + QPointF scenePos2 = item->scenePos(); + + if (item==nullptr) + { + return; + } + + QPointF scenePt = canvas()->mapToScene(e->pos()); + int x_scenePt = static_cast(std::floor(scenePt.x())); + int y_scenePt = static_cast(std::floor(scenePt.y())); + + QPointF itemPt = item->mapFromScene(scenePt); + int x_itemPt = static_cast(std::floor(itemPt.x())); + int y_itemPt = static_cast(std::floor(itemPt.y())); auto* imageLayer = canvas()->imageLayer(); RasterLayer* rl = imageLayer ? imageLayer->layer() : nullptr; - if (rl && rl->isValidPixel(x, y)) + if (rl && rl->isValidPixel(x_itemPt, y_itemPt)) { - canvas()->updateCrosshair(x + 0.5, y + 0.5); + canvas()->updateCrosshair(x_scenePt + 0.5, y_scenePt + 0.5); QVector wavelengths; QVector spectrum; - if (rl->readPixelSpectrum(x, y, wavelengths, spectrum)) + if (rl->readPixelSpectrum(x_itemPt, y_itemPt, wavelengths, spectrum)) { - emit spectralClicked(x, y, wavelengths, spectrum); + emit spectralClicked(x_itemPt, y_itemPt, wavelengths, spectrum); } } }