在医疗图像处理、三维建模等领域,我们经常需要在 VTK (Visualization Toolkit) 中利用相机正射投影将二维图像与三维模型进行对应。一个常见的需求是:已知多个 2D 坐标(例如鼠标点击位置),如何反算出对应的 3D 空间坐标? 这篇文章将深入探讨这一问题的解决方案,并提供实战经验。
问题场景重现:2D 坐标到 3D 坐标的转换
假设我们有一个 VTK 渲染窗口,使用正射投影相机,用户通过鼠标在窗口中点击了若干个点,得到了这些点的 2D 坐标(x, y)。我们需要将这些 2D 坐标转换成 3D 空间中的坐标(x, y, z),以便进行后续的三维分析,例如测量距离、绘制 3D 标注等。
底层原理深度剖析:正射投影与逆变换
正射投影是一种平行投影,其特点是投影线与投影面垂直。在 VTK 中,vtkCamera 类提供了控制相机视口、投影方式等参数的方法。要将 2D 坐标转换成 3D 坐标,本质上需要进行一次逆变换,即从屏幕坐标系转换到世界坐标系。
这个过程涉及到以下几个关键步骤:
- 屏幕坐标系到视口坐标系的转换:将 2D 屏幕坐标转换为视口坐标,需要考虑窗口大小、像素坐标原点等因素。
- 视口坐标系到裁剪坐标系的转换:视口坐标是一个归一化的坐标范围,通常在 [-1, 1] 之间。我们需要将视口坐标转换为裁剪坐标,这一步涉及到投影矩阵的逆矩阵。
- 裁剪坐标系到世界坐标系的转换:最后,将裁剪坐标转换为世界坐标,需要考虑相机的位置、方向、缩放等参数。这一步涉及到模型视图矩阵的逆矩阵。
具体代码解决方案:VTK 实现 2D 到 3D 坐标转换
以下是一个示例代码,展示了如何使用 VTK 实现 2D 坐标到 3D 坐标的转换。
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkCamera.h>
#include <vtkSphereSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkWorldPointPicker.h>
#include <vtkProperty.h>
int main()
{
// 创建渲染器、渲染窗口和交互器
vtkRenderer *renderer = vtkRenderer::New();
vtkRenderWindow *renderWindow = vtkRenderWindow::New();
renderWindow->AddRenderer(renderer);
vtkRenderWindowInteractor *interactor = vtkRenderWindowInteractor::New();
interactor->SetRenderWindow(renderWindow);
// 创建一个球体
vtkSphereSource *sphereSource = vtkSphereSource::New();
sphereSource->SetCenter(0.0, 0.0, 0.0);
sphereSource->SetRadius(1.0);
sphereSource->Update();
vtkPolyDataMapper *mapper = vtkPolyDataMapper::New();
mapper->SetInputConnection(sphereSource->GetOutputPort());
vtkActor *actor = vtkActor::New();
actor->SetMapper(mapper);
renderer->AddActor(actor);
// 设置相机为正射投影
vtkCamera *camera = renderer->GetActiveCamera();
camera->SetParallelProjection(true);
// 设置相机位置和焦点
camera->SetPosition(0, 0, 5);
camera->SetFocalPoint(0, 0, 0);
renderer->ResetCamera();
// 创建 WorldPointPicker 用于拾取 3D 坐标
vtkWorldPointPicker *picker = vtkWorldPointPicker::New();
interactor->SetPicker(picker);
// 定义一个鼠标点击事件的回调函数
interactor->AddObserver(vtkCommand::LeftButtonPressEvent, [](vtkObject* caller, unsigned long eventId, void* clientData, void* callData) {
vtkRenderWindowInteractor *iren = static_cast<vtkRenderWindowInteractor*>(caller);
vtkWorldPointPicker *picker = static_cast<vtkWorldPointPicker*>(iren->GetPicker());
int x = iren->GetEventPosition()[0];
int y = iren->GetEventPosition()[1];
// 使用 Pick3DPoint 方法获取 3D 坐标
picker->Pick(x, iren->GetRenderWindow()->GetYSize() - y - 1, 0, iren->GetRenderer()); // 注意 Y 坐标需要翻转
double *pos = picker->GetPickPosition();
std::cout << "Clicked at 2D: (" << x << ", " << y << ")\n";
std::cout << "Picked 3D: (" << pos[0] << ", " << pos[1] << ", " << pos[2] << ")\n";
// 可视化拾取到的 3D 点 (可选)
vtkSphereSource *pickedSphere = vtkSphereSource::New();
pickedSphere->SetCenter(pos[0], pos[1], pos[2]);
pickedSphere->SetRadius(0.1);
pickedSphere->Update();
vtkPolyDataMapper *pickedMapper = vtkPolyDataMapper::New();
pickedMapper->SetInputConnection(pickedSphere->GetOutputPort());
vtkActor *pickedActor = vtkActor::New();
pickedActor->SetMapper(pickedMapper);
pickedActor->GetProperty()->SetColor(1, 0, 0); // 红色
static_cast<vtkRenderer*>(clientData)->AddActor(pickedActor);
pickedSphere->Delete();
pickedMapper->Delete();
pickedActor->Delete();
iren->GetRenderWindow()->Render();
}, renderer);
// 初始化交互器并开始渲染
renderer->SetBackground(0.1, 0.2, 0.4);
renderWindow->SetSize(600, 600);
renderWindow->Render();
interactor->Initialize();
interactor->Start();
// 清理资源
sphereSource->Delete();
mapper->Delete();
actor->Delete();
camera->Delete();
renderer->Delete();
renderWindow->Delete();
interactor->Delete();
picker->Delete();
return 0;
}
代码解析:
- 创建了 VTK 渲染管线,包括渲染器、渲染窗口和交互器。
- 创建了一个球体作为示例几何体。
- 设置相机为正射投影,并调整相机的位置和焦点。
- 使用
vtkWorldPointPicker类来拾取 3D 坐标。Pick3DPoint函数利用渲染器的相机参数和鼠标点击位置,计算得到对应的 3D 坐标。 - 鼠标点击事件的回调函数中,首先获取鼠标的 2D 坐标,然后调用
picker->Pick()方法进行拾取。注意,VTK 的 Y 轴方向与屏幕坐标系相反,需要进行 Y 坐标的翻转。 - 为了验证拾取的 3D 坐标是否正确,我们在拾取到的位置绘制了一个小的红色球体。
实战避坑经验总结
- Y 轴翻转: VTK 的 Y 轴方向与屏幕坐标系相反,务必注意 Y 坐标的翻转,否则拾取到的 3D 坐标会不正确。
- 深度值的理解: 在正射投影中,所有投影线的深度值都是相同的。因此,仅仅通过 2D 坐标是无法确定 3D 坐标的深度的。通常我们需要假设一个深度值,例如将深度值设为 0,或者使用其他方法来确定深度值。
vtkWorldPointPicker默认使用的深度值可能需要根据实际场景进行调整。 - 相机参数的正确设置: 相机的位置、方向、缩放等参数对坐标转换的结果有很大影响。确保相机参数设置正确,才能得到正确的 3D 坐标。
- 性能优化: 如果需要频繁进行 2D 到 3D 坐标的转换,可以考虑缓存相机参数,避免重复计算。另外,可以使用多线程来加速计算。
- 坐标系转换的理解:深刻理解屏幕坐标系、视口坐标系、裁剪坐标系和世界坐标系之间的转换关系,是解决此类问题的关键。
希望本文能够帮助读者理解 VTK 相机正射投影中,通过多个 2D 坐标计算 3D 坐标的方法,并能在实际项目中灵活应用。同时,需要注意在使用 Nginx 这类反向代理服务器时,需要考虑在高并发场景下的性能优化,例如调整 worker 进程数、使用 epoll 模型、设置合理的缓存策略等。 如果使用了宝塔面板,也需要关注其资源占用情况,避免影响服务器的整体性能。 这些后端知识在构建高性能应用时至关重要。
冠军资讯
代码一只喵