DoseCompare/src/SliceViewWidget.cpp

1047 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "SliceViewWidget.h"
#include <QVTKOpenGLWidget.h>
#include <QVBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QResizeEvent>
#include <QPainter>
#include <QPaintEvent>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkImageActor.h>
#include <vtkImageData.h>
#include <vtkImageMapToColors.h>
#include <vtkLookupTable.h>
#include <vtkImageMapper3D.h>
#include <vtkInteractorStyleUser.h>
#include <vtkActor2D.h>
#include <vtkPolyDataMapper2D.h>
#include <vtkPolyData.h>
#include <vtkPoints.h>
#include <vtkCellArray.h>
#include <vtkProperty2D.h>
#include <vtkCamera.h>
#include <algorithm>
#include <cmath>
namespace {
vtkSmartPointer<vtkActor2D> makeCrossActor(const double rgb[3])
{
auto poly = vtkSmartPointer<vtkPolyData>::New();
auto pts = vtkSmartPointer<vtkPoints>::New();
pts->InsertNextPoint(0, 0, 0);
pts->InsertNextPoint(1, 0, 0);
auto lines = vtkSmartPointer<vtkCellArray>::New();
lines->InsertNextCell(2);
lines->InsertCellPoint(0);
lines->InsertCellPoint(1);
poly->SetPoints(pts);
poly->SetLines(lines);
auto mapper = vtkSmartPointer<vtkPolyDataMapper2D>::New();
mapper->SetInputData(poly);
auto actor = vtkSmartPointer<vtkActor2D>::New();
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(rgb[0], rgb[1], rgb[2]);
actor->GetProperty()->SetLineWidth(1.8);
return actor;
}
QLabel* makeEdgeLabel(QWidget* parent, const QString& text, const QColor& color)
{
auto* lab = new QLabel(text, parent);
lab->setStyleSheet(QStringLiteral(
"QLabel { color: %1; font: bold 14px 'Segoe UI'; background: transparent; }")
.arg(color.name()));
lab->setAttribute(Qt::WA_TransparentForMouseEvents);
lab->raise();
return lab;
}
} // namespace
ViewAxis SliceViewWidget::colorForLineDirection(const double dir[3])
{
int ax = 0;
if (std::abs(dir[1]) >= std::abs(dir[0]) && std::abs(dir[1]) >= std::abs(dir[2]))
ax = 1;
else if (std::abs(dir[2]) >= std::abs(dir[0]) && std::abs(dir[2]) >= std::abs(dir[1]))
ax = 2;
if (ax == 0)
return ViewAxis::Sagittal;
if (ax == 1)
return ViewAxis::Coronal;
return ViewAxis::Axial;
}
SliceViewWidget::SliceViewWidget(ViewAxis axis, QWidget* parent)
: QWidget(parent)
, m_axis(axis)
{
// 必须开 StyledBackground否则 stylesheet 边框经常不画
setAttribute(Qt::WA_StyledBackground, true);
setAutoFillBackground(true);
applyBorderStyle();
auto* layout = new QVBoxLayout(this);
// 给色框留出空隙VTK OpenGL 子窗体会盖住无 margin 的边)
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(0);
m_vtkWidget = new QVTKOpenGLWidget(this);
layout->addWidget(m_vtkWidget);
auto window = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
m_vtkWidget->SetRenderWindow(window);
m_renderer = vtkSmartPointer<vtkRenderer>::New();
m_renderer->SetBackground(0.08, 0.08, 0.1);
window->AddRenderer(m_renderer);
m_lutBg = vtkSmartPointer<vtkLookupTable>::New();
m_lutFg = vtkSmartPointer<vtkLookupTable>::New();
m_mapBg = vtkSmartPointer<vtkImageMapToColors>::New();
m_mapFg = vtkSmartPointer<vtkImageMapToColors>::New();
m_actorBg = vtkSmartPointer<vtkImageActor>::New();
m_actorFg = vtkSmartPointer<vtkImageActor>::New();
rebuildColorMaps();
double rgbH[3], rgbV[3];
axisColorRgb(ViewAxis::Axial, rgbH);
axisColorRgb(ViewAxis::Sagittal, rgbV);
m_crossH = makeCrossActor(rgbH);
m_crossV = makeCrossActor(rgbV);
m_renderer->AddActor2D(m_crossH);
m_renderer->AddActor2D(m_crossV);
const QColor ac = axisColor(m_axis);
QString top, bottom, left, right;
switch (m_axis) {
case ViewAxis::Axial:
top = QStringLiteral("A");
bottom = QStringLiteral("P");
left = QStringLiteral("L");
right = QStringLiteral("R");
break;
case ViewAxis::Sagittal:
top = QStringLiteral("S");
bottom = QStringLiteral("I");
left = QStringLiteral("P");
right = QStringLiteral("A");
break;
case ViewAxis::Coronal:
top = QStringLiteral("S");
bottom = QStringLiteral("I");
left = QStringLiteral("L");
right = QStringLiteral("R");
break;
}
m_labelTop = makeEdgeLabel(this, top, ac);
m_labelBottom = makeEdgeLabel(this, bottom, ac);
m_labelLeft = makeEdgeLabel(this, left, ac);
m_labelRight = makeEdgeLabel(this, right, ac);
m_pickLabel = new QLabel(this);
m_pickLabel->setStyleSheet(QStringLiteral(
"QLabel { color: #f0f0f0; font: 12px 'Consolas'; background: rgba(0,0,0,140);"
" padding: 4px 6px; border-radius: 3px; }"));
m_pickLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
m_pickLabel->hide();
m_pickLabel->raise();
auto style = vtkSmartPointer<vtkInteractorStyleUser>::New();
m_vtkWidget->GetInteractor()->SetInteractorStyle(style);
m_vtkWidget->installEventFilter(this);
m_vtkWidget->setMouseTracking(true);
setMouseTracking(true);
updateOrientationLabels();
}
void SliceViewWidget::applyBorderStyle()
{
const QColor c = axisColor(m_axis);
QPalette pal = palette();
pal.setColor(QPalette::Window, c);
setPalette(pal);
setStyleSheet(QStringLiteral(
"SliceViewWidget {"
" border: 1px solid %1;"
" background-color: %1;"
"}")
.arg(c.name()));
}
void SliceViewWidget::paintEvent(QPaintEvent* event)
{
QWidget::paintEvent(event);
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, false);
p.setPen(QPen(axisColor(m_axis), 1));
p.setBrush(Qt::NoBrush);
p.drawRect(rect().adjusted(0, 0, -1, -1));
}
namespace {
void fillLookupTable(vtkLookupTable* lut, DoseColorMap map, double lo, double hi)
{
if (!lut)
return;
if (!(hi > lo))
hi = lo + 1.0;
lut->SetNumberOfTableValues(256);
lut->SetRange(lo, hi);
auto clamp01 = [](double x) {
return std::max(0.0, std::min(1.0, x));
};
for (int i = 0; i < 256; ++i) {
const double t = i / 255.0;
double r = t, g = t, b = t;
switch (map) {
case DoseColorMap::Gray:
r = g = b = t;
break;
case DoseColorMap::Rainbow: {
// 蓝→青→绿→黄→红
const double h = (1.0 - t) * 0.6667;
const double s = 1.0;
const double v = 1.0;
const int sector = static_cast<int>(h * 6.0) % 6;
const double f = h * 6.0 - std::floor(h * 6.0);
const double p = v * (1.0 - s);
const double q = v * (1.0 - s * f);
const double u = v * (1.0 - s * (1.0 - f));
switch (sector) {
case 0: r = v; g = u; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = u; break;
case 3: r = p; g = q; b = v; break;
case 4: r = u; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
break;
}
case DoseColorMap::Jet: {
// 经典 jet
r = clamp01(1.5 - std::abs(4.0 * t - 3.0));
g = clamp01(1.5 - std::abs(4.0 * t - 2.0));
b = clamp01(1.5 - std::abs(4.0 * t - 1.0));
break;
}
case DoseColorMap::Hot: {
r = clamp01(t * 2.5);
g = clamp01(t * 2.5 - 1.0);
b = clamp01(t * 2.5 - 2.0);
break;
}
}
lut->SetTableValue(i, r, g, b, 1.0);
}
lut->Build();
}
} // namespace
void SliceViewWidget::rebuildColorMaps()
{
const double lo = m_level - 0.5 * m_window;
const double hi = m_level + 0.5 * m_window;
fillLookupTable(m_lutBg, m_colorMap, lo, hi);
fillLookupTable(m_lutFg, m_colorMap, lo, hi);
if (m_mapBg)
m_mapBg->SetLookupTable(m_lutBg);
if (m_mapFg)
m_mapFg->SetLookupTable(m_lutFg);
}
void SliceViewWidget::refreshMappedImages()
{
// 无切片输入时禁止 Update否则 vtkImageMapToColors 会报 0 connections
if (m_bgSlice && m_mapBg) {
m_mapBg->SetInputData(m_bgSlice);
m_mapBg->SetLookupTable(m_lutBg);
m_mapBg->SetOutputFormatToRGB();
m_mapBg->Update();
if (auto* mapper = m_actorBg->GetMapper())
mapper->SetInputConnection(m_mapBg->GetOutputPort());
}
if (m_fgSlice && m_mapFg) {
m_mapFg->SetInputData(m_fgSlice);
m_mapFg->SetLookupTable(m_lutFg);
m_mapFg->SetOutputFormatToRGB();
m_mapFg->Update();
if (auto* mapper = m_actorFg->GetMapper())
mapper->SetInputConnection(m_mapFg->GetOutputPort());
}
if (m_bgSlice && m_vtkWidget && m_vtkWidget->GetRenderWindow())
m_vtkWidget->GetRenderWindow()->Render();
}
void SliceViewWidget::setColorMap(DoseColorMap map)
{
m_colorMap = map;
rebuildColorMaps();
refreshMappedImages();
}
void SliceViewWidget::viewDirections(double u[3], double v[3], double n[3]) const
{
// 物理轴按 LPS 显示(+X=L +Y=P +Z=S。MHD 虽写 RAI但按 LPS 才能上A下P。
// 轴位上A下P左L右R —— 「屁股朝天」= P 在上,必须用 v=-YA
for (int i = 0; i < 3; ++i)
u[i] = v[i] = n[i] = 0.0;
switch (m_axis) {
case ViewAxis::Axial:
u[0] = -1.0; // 屏右 = R = -X
v[1] = -1.0; // 屏上 = A = -Y+Y 是屁股 P
n[2] = 1.0; // 从头(S)往脚(I)看
break;
case ViewAxis::Sagittal:
// 从患者右看上S下I 左P右A
u[1] = -1.0; // 右 = A = -Y
v[2] = 1.0; // 上 = S = +Z
n[0] = -1.0; // 患者右 = -X从右侧看
break;
case ViewAxis::Coronal:
// 面朝患者、约定俗成上S下I 左L右R图像水平翻转
u[0] = -1.0; // 屏右 = R = -X屏左 = L = +X
v[2] = 1.0; // 上 = S = +Z
n[1] = -1.0; // 从 A(-Y) 看向 P
break;
}
}
void SliceViewWidget::inPlaneWorldAxes(int& uAxis, int& vAxis) const
{
switch (m_axis) {
case ViewAxis::Axial:
uAxis = 0;
vAxis = 1;
break;
case ViewAxis::Sagittal:
uAxis = 1;
vAxis = 2;
break;
case ViewAxis::Coronal:
uAxis = 0;
vAxis = 2;
break;
}
}
void SliceViewWidget::updateOrientationLabels()
{
if (!m_labelTop)
return;
const int m = 6;
m_labelTop->move((width() - m_labelTop->sizeHint().width()) / 2, m);
m_labelBottom->move((width() - m_labelBottom->sizeHint().width()) / 2,
height() - m_labelBottom->sizeHint().height() - m);
m_labelLeft->move(m, (height() - m_labelLeft->sizeHint().height()) / 2);
m_labelRight->move(width() - m_labelRight->sizeHint().width() - m,
(height() - m_labelRight->sizeHint().height()) / 2);
m_labelTop->raise();
m_labelBottom->raise();
m_labelLeft->raise();
m_labelRight->raise();
m_pickLabel->raise();
}
void SliceViewWidget::setBackground(DoseImage::Pointer bg)
{
m_bg = std::move(bg);
rebuildPipeline();
}
void SliceViewWidget::setFront(DoseImage::Pointer fg)
{
m_fg = std::move(fg);
if (!m_bg) {
// 无背景时不必单独建 front
return;
}
// 轻量更新:不拆整管线
if (m_fg) {
m_renderer->RemoveActor(m_actorFg);
m_renderer->AddActor(m_actorFg);
m_fgSlice = samplePhysicalSlice(m_fg);
rebuildColorMaps();
m_mapFg->SetInputData(m_fgSlice);
m_mapFg->SetLookupTable(m_lutFg);
m_mapFg->SetOutputFormatToRGB();
m_mapFg->Update();
if (auto* mapper = m_actorFg->GetMapper())
mapper->SetInputConnection(m_mapFg->GetOutputPort());
m_actorFg->SetOpacity(m_frontOpacity);
m_actorFg->InterpolateOn();
} else {
m_renderer->RemoveActor(m_actorFg);
m_fgSlice = nullptr;
}
m_vtkWidget->GetRenderWindow()->Render();
}
void SliceViewWidget::setOpacity(double frontOpacity)
{
m_frontOpacity = std::max(0.0, std::min(1.0, frontOpacity));
if (m_fg) {
m_actorFg->SetOpacity(m_frontOpacity);
m_actorBg->SetOpacity(1.0);
} else {
m_actorBg->SetOpacity(1.0);
}
m_vtkWidget->GetRenderWindow()->Render();
}
Vec3d SliceViewWidget::sliceOrigin() const
{
// m_viewCenter = 体中心(仅加载/复位时设置),平移只动相机、绝不改它。
// 否则pan 把 viewCenter 写成 ori+fp滚轮重采样后再叠加相机 fp → 双倍偏移,放大后看不见图。
double u[3], v[3], n[3];
viewDirections(u, v, n);
const double dx = m_cursor.x - m_viewCenter.x;
const double dy = m_cursor.y - m_viewCenter.y;
const double dz = m_cursor.z - m_viewCenter.z;
const double dn = dx * n[0] + dy * n[1] + dz * n[2];
return {m_viewCenter.x + dn * n[0], m_viewCenter.y + dn * n[1],
m_viewCenter.z + dn * n[2]};
}
void SliceViewWidget::physicalStepUV(double& su, double& sv) const
{
if (!m_bg) {
su = sv = 1.0;
return;
}
// 用 ITK spacing 在 U/V 上的物理步长(取主导分量)
const auto sp = m_bg->spacing();
double u[3], v[3], n[3];
viewDirections(u, v, n);
su = std::max(1e-3, std::abs(u[0]) * std::abs(sp[0]) + std::abs(u[1]) * std::abs(sp[1])
+ std::abs(u[2]) * std::abs(sp[2]));
sv = std::max(1e-3, std::abs(v[0]) * std::abs(sp[0]) + std::abs(v[1]) * std::abs(sp[1])
+ std::abs(v[2]) * std::abs(sp[2]));
}
void SliceViewWidget::sliceSizeUV(double& halfU, double& halfV) const
{
halfU = halfV = 50.0;
if (!m_bg)
return;
double bmin[3], bmax[3];
m_bg->worldBounds(bmin, bmax);
double u[3], v[3], n[3];
viewDirections(u, v, n);
const Vec3d ori = sliceOrigin();
halfU = halfV = 0.0;
for (int iz = 0; iz < 2; ++iz)
for (int iy = 0; iy < 2; ++iy)
for (int ix = 0; ix < 2; ++ix) {
const double px = (ix ? bmax[0] : bmin[0]) - ori.x;
const double py = (iy ? bmax[1] : bmin[1]) - ori.y;
const double pz = (iz ? bmax[2] : bmin[2]) - ori.z;
halfU = std::max(halfU, std::abs(px * u[0] + py * u[1] + pz * u[2]));
halfV = std::max(halfV, std::abs(px * v[0] + py * v[1] + pz * v[2]));
}
halfU = std::max(halfU, 1.0);
halfV = std::max(halfV, 1.0);
}
vtkSmartPointer<vtkImageData> SliceViewWidget::samplePhysicalSlice(
const DoseImage::Pointer& dose) const
{
if (!dose)
return nullptr;
double u[3], v[3], n[3];
viewDirections(u, v, n);
const Vec3d ori = sliceOrigin();
double su = m_su, sv = m_sv;
// 始终按体数据全范围采样;缩放只改相机,绝不缩小采样半宽(否则滚轮切层会坏)
double halfU = 0.0, halfV = 0.0;
sliceSizeUV(halfU, halfV);
int nu = static_cast<int>(std::ceil(2.0 * halfU / su)) + 1;
int nv = static_cast<int>(std::ceil(2.0 * halfV / sv)) + 1;
nu = std::max(nu, 8);
nv = std::max(nv, 8);
// 限制分辨率,避免过大
const int kMax = 1024;
if (nu > kMax) {
su *= static_cast<double>(nu) / kMax;
nu = kMax;
}
if (nv > kMax) {
sv *= static_cast<double>(nv) / kMax;
nv = kMax;
}
auto img = vtkSmartPointer<vtkImageData>::New();
img->SetDimensions(nu, nv, 1);
img->SetSpacing(su, sv, 1.0);
// 2D 局部坐标:中心在 (0,0),便于相机固定看 XY
img->SetOrigin(-0.5 * (nu - 1) * su, -0.5 * (nv - 1) * sv, 0.0);
img->AllocateScalars(VTK_FLOAT, 1);
auto* ptr = static_cast<float*>(img->GetScalarPointer());
const double x0 = -0.5 * (nu - 1) * su;
const double y0 = -0.5 * (nv - 1) * sv;
for (int j = 0; j < nv; ++j) {
const double tv = y0 + j * sv;
for (int i = 0; i < nu; ++i) {
const double tu = x0 + i * su;
const double wx = ori.x + tu * u[0] + tv * v[0];
const double wy = ori.y + tu * u[1] + tv * v[1];
const double wz = ori.z + tu * u[2] + tv * v[2];
ptr[j * nu + i] = dose->sampleWorld(wx, wy, wz);
}
}
return img;
}
void SliceViewWidget::setCursorWorld(const Vec3d& world)
{
if (!m_bg) {
m_cursor = world;
m_hasCursor = true;
return;
}
const Vec3d oldOri = sliceOrigin();
m_cursor = world;
m_hasCursor = true;
const Vec3d newOri = sliceOrigin();
const double d2 = (oldOri.x - newOri.x) * (oldOri.x - newOri.x)
+ (oldOri.y - newOri.y) * (oldOri.y - newOri.y)
+ (oldOri.z - newOri.z) * (oldOri.z - newOri.z);
if (d2 > 1e-8)
updateSlice(true);
updateCrosshair();
m_vtkWidget->GetRenderWindow()->Render();
}
void SliceViewWidget::resetViewToVolumeCenter()
{
if (!m_bg)
return;
double bmin[3], bmax[3];
m_bg->worldBounds(bmin, bmax);
m_cursor = {(bmin[0] + bmax[0]) * 0.5, (bmin[1] + bmax[1]) * 0.5,
(bmin[2] + bmax[2]) * 0.5};
m_viewCenter = m_cursor;
m_hasCursor = true;
physicalStepUV(m_su, m_sv);
sliceSizeUV(m_halfU, m_halfV);
updateSlice(false);
setupCamera(true);
updateCrosshair();
m_vtkWidget->GetRenderWindow()->Render();
emit viewExtentChanged();
}
void SliceViewWidget::setWindowLevel(double window, double level)
{
m_window = window;
m_level = level;
rebuildColorMaps();
refreshMappedImages();
}
void SliceViewWidget::resetWindowLevelToData()
{
if (!m_bg)
return;
m_window = static_cast<double>(m_bg->maxValue() - m_bg->minValue());
m_level = static_cast<double>((m_bg->maxValue() + m_bg->minValue()) * 0.5);
if (m_window < 1e-6)
m_window = 1.0;
rebuildColorMaps();
emit windowLevelChanged(m_window, m_level);
}
bool SliceViewWidget::visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const
{
if (!m_renderer || !m_vtkWidget || !m_cameraReady)
return false;
int uAxis = 0, vAxis = 1;
inPlaneWorldAxes(uAxis, vAxis);
if (worldAxis != uAxis && worldAxis != vAxis)
return false;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return false;
const double halfH = cam->GetParallelScale();
const double aspect = static_cast<double>(std::max(1, m_vtkWidget->width()))
/ static_cast<double>(std::max(1, m_vtkWidget->height()));
const double halfW = halfH * aspect;
double fp[3];
cam->GetFocalPoint(fp);
double u[3], v[3], n[3];
viewDirections(u, v, n);
const Vec3d ori = sliceOrigin();
// 可视中心 = 切片原点 + 相机焦平面内偏移(与 pan 一致)
const double cx = ori.x + fp[0] * u[0] + fp[1] * v[0];
const double cy = ori.y + fp[0] * u[1] + fp[1] * v[1];
const double cz = ori.z + fp[0] * u[2] + fp[1] * v[2];
const double center[3] = {cx, cy, cz};
const double half = (worldAxis == uAxis) ? halfW : halfH;
rmin = center[worldAxis] - half;
rmax = center[worldAxis] + half;
return true;
}
bool SliceViewWidget::displayToWorld(int displayX, int displayY, Vec3d& out) const
{
if (!m_renderer || !m_cameraReady)
return false;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return false;
const int w = std::max(1, m_vtkWidget->width());
const int h = std::max(1, m_vtkWidget->height());
const double halfH = cam->GetParallelScale();
const double halfW = halfH * static_cast<double>(w) / static_cast<double>(h);
double fp[3];
cam->GetFocalPoint(fp);
// 切片画在局部 XYfp 约在 (0,0,0)U→XV→Y
const double du = (displayX - w * 0.5) / (w * 0.5) * halfW + fp[0];
const double dv = (h * 0.5 - displayY) / (h * 0.5) * halfH + fp[1];
double u[3], v[3], n[3];
viewDirections(u, v, n);
const Vec3d ori = sliceOrigin();
out.x = ori.x + du * u[0] + dv * v[0];
out.y = ori.y + du * u[1] + dv * v[1];
out.z = ori.z + du * u[2] + dv * v[2];
return true;
}
void SliceViewWidget::rebuildPipeline()
{
m_renderer->RemoveActor(m_actorBg);
m_renderer->RemoveActor(m_actorFg);
m_bgSlice = nullptr;
m_fgSlice = nullptr;
m_cameraReady = false;
if (!m_bg) {
if (m_vtkWidget && m_vtkWidget->GetRenderWindow())
m_vtkWidget->GetRenderWindow()->Render();
return;
}
double bmin[3], bmax[3];
m_bg->worldBounds(bmin, bmax);
m_cursor = {(bmin[0] + bmax[0]) * 0.5, (bmin[1] + bmax[1]) * 0.5,
(bmin[2] + bmax[2]) * 0.5};
m_viewCenter = m_cursor;
m_hasCursor = true;
physicalStepUV(m_su, m_sv);
sliceSizeUV(m_halfU, m_halfV);
m_renderer->AddActor(m_actorBg);
if (m_fg)
m_renderer->AddActor(m_actorFg);
resetWindowLevelToData();
updateSlice(false);
setupCamera(true);
updateCrosshair();
m_vtkWidget->GetRenderWindow()->Render();
emit viewExtentChanged();
}
void SliceViewWidget::updateSlice(bool /*moveCamera*/)
{
if (!m_bg)
return;
// 保留相机(缩放/平移),切层绝不能 reset 相机
double savedScale = 1.0;
double savedFp[3] = {0, 0, 0};
double savedPos[3] = {0, 0, 1};
double savedUp[3] = {0, 1, 0};
auto* cam = m_renderer ? m_renderer->GetActiveCamera() : nullptr;
if (cam && m_cameraReady) {
savedScale = cam->GetParallelScale();
cam->GetFocalPoint(savedFp);
cam->GetPosition(savedPos);
cam->GetViewUp(savedUp);
}
physicalStepUV(m_su, m_sv);
// 采样范围始终用体全图,与相机缩放解耦
sliceSizeUV(m_halfU, m_halfV);
m_bgSlice = samplePhysicalSlice(m_bg);
if (!m_bgSlice)
return;
rebuildColorMaps();
m_mapBg->SetInputData(m_bgSlice);
m_mapBg->SetLookupTable(m_lutBg);
m_mapBg->SetOutputFormatToRGB();
m_mapBg->Update();
if (auto* mapper = m_actorBg->GetMapper()) {
mapper->SetInputConnection(m_mapBg->GetOutputPort());
}
m_actorBg->InterpolateOn();
m_actorBg->Update();
if (m_fg) {
m_fgSlice = samplePhysicalSlice(m_fg);
if (m_fgSlice) {
m_mapFg->SetInputData(m_fgSlice);
m_mapFg->SetLookupTable(m_lutFg);
m_mapFg->SetOutputFormatToRGB();
m_mapFg->Update();
if (auto* mapper = m_actorFg->GetMapper())
mapper->SetInputConnection(m_mapFg->GetOutputPort());
m_actorFg->SetOpacity(m_frontOpacity);
m_actorFg->InterpolateOn();
m_actorFg->Update();
}
}
if (cam && m_cameraReady) {
cam->SetParallelProjection(true);
cam->SetParallelScale(savedScale);
cam->SetFocalPoint(savedFp);
cam->SetPosition(savedPos);
cam->SetViewUp(savedUp);
m_renderer->ResetCameraClippingRange();
}
}
void SliceViewWidget::setupCamera(bool resetScale)
{
if (!m_renderer)
return;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
// 切片在局部 XY 平面,相机沿 +Z 看ViewUp=+Y
cam->SetParallelProjection(true);
cam->SetFocalPoint(0.0, 0.0, 0.0);
cam->SetPosition(0.0, 0.0, 1.0);
cam->SetViewUp(0.0, 1.0, 0.0);
if (resetScale) {
const double half = std::max(m_halfU, m_halfV);
cam->SetParallelScale(std::max(1.0, half * 0.55));
}
m_renderer->ResetCameraClippingRange();
m_cameraReady = true;
}
void SliceViewWidget::updateCrosshair()
{
if (!m_renderer || !m_vtkWidget || !m_cameraReady)
return;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
double u[3], v[3], n[3];
viewDirections(u, v, n);
const Vec3d ori = sliceOrigin();
const double dx = m_cursor.x - ori.x;
const double dy = m_cursor.y - ori.y;
const double dz = m_cursor.z - ori.z;
const double tu = dx * u[0] + dy * u[1] + dz * u[2];
const double tv = dx * v[0] + dy * v[1] + dz * v[2];
m_renderer->SetWorldPoint(tu, tv, 0.0, 1.0);
m_renderer->WorldToDisplay();
double disp[3];
m_renderer->GetDisplayPoint(disp);
if (!std::isfinite(disp[0]) || !std::isfinite(disp[1]))
return;
const int w = std::max(1, m_vtkWidget->width());
const int h = std::max(1, m_vtkWidget->height());
const double cx = disp[0];
const double cy = disp[1];
auto setLine = [](vtkActor2D* actor, double x0, double y0, double x1, double y1,
const double rgb[3]) {
auto* mapper = vtkPolyDataMapper2D::SafeDownCast(actor->GetMapper());
auto* poly = vtkPolyData::SafeDownCast(mapper->GetInput());
auto* pts = poly->GetPoints();
pts->SetPoint(0, x0, y0, 0);
pts->SetPoint(1, x1, y1, 0);
pts->Modified();
poly->Modified();
actor->GetProperty()->SetColor(rgb[0], rgb[1], rgb[2]);
};
double rgbU[3], rgbV[3];
axisColorRgb(colorForLineDirection(u), rgbU);
axisColorRgb(colorForLineDirection(v), rgbV);
setLine(m_crossH, 0, cy, w, cy, rgbU);
setLine(m_crossV, cx, 0, cx, h, rgbV);
}
void SliceViewWidget::updatePickOverlay(const QPoint& pos)
{
Vec3d world;
if (!displayToWorld(pos.x(), pos.y(), world) || !m_bg) {
m_pickLabel->hide();
return;
}
const float bVal = m_bg->sampleWorld(world.x, world.y, world.z);
QString text;
if (m_fg) {
const float fVal = m_fg->sampleWorld(world.x, world.y, world.z);
text = QStringLiteral("F: %1\nB: %2").arg(fVal, 0, 'f', 3).arg(bVal, 0, 'f', 3);
} else {
text = QStringLiteral("F: -\nB: %1").arg(bVal, 0, 'f', 3);
}
m_pickLabel->setText(text);
m_pickLabel->adjustSize();
m_pickLabel->move(std::max(0, width() - m_pickLabel->width() - 8),
std::max(0, height() - m_pickLabel->height() - 8));
m_pickLabel->show();
m_pickLabel->raise();
}
void SliceViewWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
updateOrientationLabels();
updateCrosshair();
emit viewExtentChanged();
}
bool SliceViewWidget::eventFilter(QObject* obj, QEvent* event)
{
if (obj != m_vtkWidget)
return QWidget::eventFilter(obj, event);
switch (event->type()) {
case QEvent::MouseButtonPress: {
auto* e = static_cast<QMouseEvent*>(event);
m_lastPos = e->pos();
if (e->button() == Qt::LeftButton) {
m_leftDown = true;
return true;
}
if (e->button() == Qt::RightButton) {
m_rightDown = true;
return true;
}
if (e->button() == Qt::MiddleButton) {
m_middleDown = true;
return true;
}
break;
}
case QEvent::MouseButtonRelease: {
auto* e = static_cast<QMouseEvent*>(event);
if (e->button() == Qt::LeftButton)
m_leftDown = false;
else if (e->button() == Qt::RightButton)
m_rightDown = false;
else if (e->button() == Qt::MiddleButton)
m_middleDown = false;
return true;
}
case QEvent::MouseMove: {
auto* e = static_cast<QMouseEvent*>(event);
const int dx = e->x() - m_lastPos.x();
const int dy = e->y() - m_lastPos.y();
m_lastPos = e->pos();
updatePickOverlay(e->pos());
// Ctrl + 移动:在鼠标位置重切另外两个面(更新交点)
if ((e->modifiers() & Qt::ControlModifier) && !m_leftDown && !m_rightDown &&
!m_middleDown) {
Vec3d world;
if (displayToWorld(e->x(), e->y(), world))
emit cursorChanged(world);
return true;
}
if (m_leftDown) {
applyMouseWindowLevel(dx, dy);
return true;
}
if (m_rightDown) {
applyPan(dx, dy);
return true;
}
if (m_middleDown) {
applyZoomByDrag(dy);
return true;
}
return true;
}
case QEvent::Leave:
m_pickLabel->hide();
break;
case QEvent::Wheel: {
auto* e = static_cast<QWheelEvent*>(event);
const int steps = (e->angleDelta().y() > 0) ? 1 : (e->angleDelta().y() < 0 ? -1 : 0);
if (steps != 0)
applySliceStep(steps);
return true;
}
default:
break;
}
return QWidget::eventFilter(obj, event);
}
void SliceViewWidget::applyMouseWindowLevel(int dx, int dy)
{
m_window = std::max(1e-3, m_window + dx * (m_window * 0.005 + 0.01));
m_level = m_level - dy * (m_window * 0.005 + 0.01);
setWindowLevel(m_window, m_level);
emit windowLevelChanged(m_window, m_level);
}
void SliceViewWidget::applyPan(int dx, int dy)
{
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
cam->SetParallelProjection(true);
double fp[3], pos[3];
cam->GetFocalPoint(fp);
cam->GetPosition(pos);
const double scale = cam->GetParallelScale() * 0.002;
const double dU = -dx * scale;
const double dV = dy * scale;
fp[0] += dU;
fp[1] += dV;
pos[0] += dU;
pos[1] += dV;
cam->SetFocalPoint(fp);
cam->SetPosition(pos);
double u[3], v[3], n[3];
viewDirections(u, v, n);
const double wdx = dU * u[0] + dV * v[0];
const double wdy = dU * u[1] + dV * v[1];
const double wdz = dU * u[2] + dV * v[2];
updateCrosshair();
m_vtkWidget->GetRenderWindow()->Render();
emit panWorldDelta(wdx, wdy, wdz);
emit viewExtentChanged();
}
void SliceViewWidget::applyZoomByDrag(int dy)
{
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
double scale = cam->GetParallelScale();
scale *= std::max(0.5, std::min(1.5, 1.0 + dy * 0.01));
scale = std::max(0.1, scale);
cam->SetParallelScale(scale);
updateCrosshair();
m_vtkWidget->GetRenderWindow()->Render();
emit zoomScaleChanged(scale);
emit viewExtentChanged();
}
double SliceViewWidget::parallelScale() const
{
if (!m_renderer || !m_cameraReady)
return 1.0;
auto* cam = m_renderer->GetActiveCamera();
return cam ? cam->GetParallelScale() : 1.0;
}
void SliceViewWidget::setParallelScaleShared(double scale)
{
if (!m_renderer || !m_cameraReady)
return;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
cam->SetParallelScale(std::max(0.1, scale));
m_renderer->ResetCameraClippingRange();
updateCrosshair();
if (m_vtkWidget && m_vtkWidget->GetRenderWindow())
m_vtkWidget->GetRenderWindow()->Render();
}
void SliceViewWidget::applyWorldPanShared(double dx, double dy, double dz)
{
if (!m_renderer || !m_cameraReady)
return;
auto* cam = m_renderer->GetActiveCamera();
if (!cam)
return;
double u[3], v[3], n[3];
viewDirections(u, v, n);
const double dU = dx * u[0] + dy * u[1] + dz * u[2];
const double dV = dx * v[0] + dy * v[1] + dz * v[2];
if (std::abs(dU) < 1e-12 && std::abs(dV) < 1e-12)
return;
cam->SetParallelProjection(true);
double fp[3], pos[3];
cam->GetFocalPoint(fp);
cam->GetPosition(pos);
fp[0] += dU;
fp[1] += dV;
pos[0] += dU;
pos[1] += dV;
cam->SetFocalPoint(fp);
cam->SetPosition(pos);
updateCrosshair();
if (m_vtkWidget && m_vtkWidget->GetRenderWindow())
m_vtkWidget->GetRenderWindow()->Render();
}
void SliceViewWidget::applySliceStep(int steps)
{
if (!m_bg || steps == 0)
return;
const auto sp = m_bg->spacing();
double u[3], v[3], n[3];
viewDirections(u, v, n);
double stepLen = std::abs(sp[2]);
if (m_axis == ViewAxis::Sagittal)
stepLen = std::abs(sp[0]);
else if (m_axis == ViewAxis::Coronal)
stepLen = std::abs(sp[1]);
Vec3d c = m_cursor;
c.x += n[0] * steps * stepLen;
c.y += n[1] * steps * stepLen;
c.z += n[2] * steps * stepLen;
double bmin[3], bmax[3];
m_bg->worldBounds(bmin, bmax);
c.x = std::max(bmin[0], std::min(bmax[0], c.x));
c.y = std::max(bmin[1], std::min(bmax[1], c.y));
c.z = std::max(bmin[2], std::min(bmax[2], c.z));
emit cursorChanged(c);
}