1047 lines
29 KiB
C++
1047 lines
29 KiB
C++
#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=-Y(A)
|
||
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);
|
||
|
||
// 切片画在局部 XY:fp 约在 (0,0,0),U→X,V→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);
|
||
}
|