mapcavas添加功能:镜像和旋转,并保存配置
This commit is contained in:
tangchao0503
2026-09-18 14:31:28 +08:00
parent 83adaa8b8b
commit 2747d5f967
8 changed files with 422 additions and 42 deletions

View File

@ -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<int>(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);
}

View File

@ -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;
};

View File

@ -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<Mapcavas*> canvases = currentWidget->findChildren<Mapcavas*>();
if (canvases.isEmpty()) return;
canvases[0]->rotateImage90();
});
connect(ui.mActionVerticalMirror, &QAction::triggered, this, [=](bool) {
QWidget* currentWidget = m_imageViewerTabWidget->currentWidget();
if (!currentWidget) return;
QList<Mapcavas*> canvases = currentWidget->findChildren<Mapcavas*>();
if (canvases.isEmpty()) return;
canvases[0]->toggleVerticalMirror();
});
connect(ui.mActionHorizontalMirror, &QAction::triggered, this, [=](bool) {
QWidget* currentWidget = m_imageViewerTabWidget->currentWidget();
if (!currentWidget) return;
QList<Mapcavas*> canvases = currentWidget->findChildren<Mapcavas*>();
if (canvases.isEmpty()) return;
canvases[0]->toggleHorizontalMirror();
});
// 使用样式表设置透明背景
toolBar->setStyleSheet(R"(
QToolBar {

View File

@ -32,6 +32,9 @@
<file>resources/icons/svg/tree_tri_down.svg</file>
<file>resources/icons/svg/tree_tri_right.svg</file>
<file>resources/icons/svg/mIconRaster.svg</file>
<file>resources/icons/svg/HorizontalMirror.svg</file>
<file>resources/icons/svg/rotate90.svg</file>
<file>resources/icons/svg/VerticalMirror.svg</file>
</qresource>
<qresource prefix="/png">
<file>resources/icons/png/Spectral_Insight_27.png</file>

View File

@ -203,6 +203,9 @@ QToolBar QToolButton:hover {
<addaction name="actionOpenDirectory"/>
<addaction name="mActionPan"/>
<addaction name="mActionSpectral"/>
<addaction name="mActionVerticalMirror"/>
<addaction name="mActionHorizontalMirror"/>
<addaction name="mActionRotation90"/>
</widget>
<widget class="QStatusBar" name="statusBar">
<property name="styleSheet">
@ -758,6 +761,27 @@ QPushButton:pressed
<string>贡嘎山旋转平台</string>
</property>
</action>
<action name="mActionVerticalMirror">
<property name="text">
<string>上下镜像</string>
</property>
</action>
<action name="mActionHorizontalMirror">
<property name="text">
<string>左右镜像</string>
</property>
<property name="toolTip">
<string>左右镜像</string>
</property>
</action>
<action name="mActionRotation90">
<property name="text">
<string>旋转90度</string>
</property>
<property name="toolTip">
<string>旋转90度</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -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);
}

View File

@ -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<double>, QVector<double>);
};

View File

@ -37,21 +37,35 @@ void MapToolSpectral::canvasMousePressEvent(QMouseEvent* e)
if (!canvas())
return;
const QPointF scenePt = canvas()->mapToScene(e->pos());
const int x = static_cast<int>(std::floor(scenePt.x()));
const int y = static_cast<int>(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<int>(std::floor(scenePt.x()));
int y_scenePt = static_cast<int>(std::floor(scenePt.y()));
QPointF itemPt = item->mapFromScene(scenePt);
int x_itemPt = static_cast<int>(std::floor(itemPt.x()));
int y_itemPt = static_cast<int>(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<double> wavelengths;
QVector<double> 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);
}
}
}