基于C++/VTK/Boost的牙科医疗影像处理与手术辅助支持系统六边形架构分析与设计
摘要
牙科医疗影像处理与手术辅助支持系统(Dental Imaging Processing and Surgical Assistance System, DIPSAS)需要在异构数据源、多种成像模态(CBCT、口内扫描、全景片)、实时交互与严格医疗合规约束下运行。传统分层架构在应对领域逻辑与外部框架(VTK渲染、Boost并行、DICOM库、硬件设备)高度耦合时,易产生"框架渗透"与测试困难。本文提出以六边形架构(Ports & Adapters)为核心的组织范式,将牙科影像处理与手术导航的领域逻辑置于架构中心,通过端口定义与适配器隔离VTK、Boost、DICOM、Qt及手术器械SDK等外部依赖。文中给出基于C++17/VTK 9/Boost 1.8x的完整项目代码骨架,包括领域模型、端口契约、VTK渲染适配器、Boost.Asio设备适配器、依赖注入组合根及GoogleTest测试适配器,并从依赖倒置、可测试性、可替换性与并行处理四个维度分析该架构的收益与代价。结果表明,六边形架构能够在不牺牲VTK渲染性能与Boost并行能力的前提下,显著提升领域代码的可测试性与外部技术栈的可演进性,适用于医疗影像类长生命周期系统。
关键词:六边形架构;端口与适配器;C++;VTK;Boost;牙科影像;手术导航;依赖倒置
1 引言
牙科数字化诊疗链条涵盖锥形束CT(CBCT)三维重建、口内扫描网格配准、种植体规划、导板设计与术中导航。此类系统的典型特征是:领域算法(配准、分割、路径规划、配准误差评估)复杂且需反复验证;外部技术栈庞杂(VTK用于体绘制与网格处理,ITK用于配准,Boost用于图算法与异步IO,Qt用于界面,各家手术器械SDK用于位姿跟踪);同时受医疗器械软件注册与IEC 62304生命周期约束,要求领域逻辑具备可追溯的单元测试覆盖。
分层架构(Controller-Service-Repository)在早期可行,但实践中常出现领域层直接包含vtkSmartPointer、boost::asio::io_context等类型,导致:领域代码无法脱离图形环境运行测试;更换渲染后端或跟踪设备时需修改业务代码;并行策略与业务逻辑纠缠。六边形架构(Hexagonal Architecture,又称Ports & Adapters)由Cockburn提出[1],其核心是以领域为中心的依赖倒置:领域定义端口(接口),外部技术以实现适配器的方式接入,依赖箭头一律指向内部。本文以DIPSAS项目为背景,论证该架构在C++医疗影像系统中的适用性,并给出可编译的代码骨架与设计权衡分析。
2 需求与架构驱动力
2.1 功能需求(摘要)
-
FR1:加载CBCT体数据(DICOM序列)与口内扫描网格(STL/PLY),完成坐标系统一。
-
FR2:颌骨与牙列分割、神经管提取、种植体路径规划。
-
FR3:术中器械位姿实时跟踪,与术前规划配准并渲染偏差。
-
FR4:手术导板与规划报告导出(STL/PDF)。
-
FR5:操作审计日志与影像数据的可追溯访问。
2.2 质量属性(架构驱动力)
| 属性 | 场景 | 架构含义 |
|---|---|---|
| 可测试性 | 配准算法须在无GPU/无显示环境CI中运行 | 领域层不得依赖VTK渲染上下文 |
| 可替换性 | 跟踪设备从光学式更换为电磁式 | 设备访问须经端口抽象 |
| 性能 | 体绘制≥30FPS,配准在秒级完成 | 适配器层可充分利用VTK/Boost并行 |
| 合规性 | IEC 62304 B/C级,需求-代码-测试可追溯 | 端口即契约,适配器可独立验证 |
| 可演进性 | DICOM库、Qt版本升级 | 技术栈变更限于适配器 |
这些驱动力共同指向"领域隔离 + 技术可插拔"的架构诉求,正是六边形架构的设计目标。
3 六边形架构总体设计
3.1 分层与依赖规则
系统划分为三个同心区域:
-
领域核心(Domain Core):纯C++17,仅依赖标准库与Boost中与IO无关的头文件(如
boost::geometry的纯计算部分需谨慎引入,实际项目中以自研几何类型为主)。包含实体、值对象、领域服务与用例。 -
端口(Ports):定义在领域边界上的抽象接口,分为入站端口(用例接口,由外部调用)与出站端口(领域所需的外部能力,如影像仓库、渲染器、跟踪设备、日志)。
-
适配器(Adapters):驱动型适配器(Qt界面、REST/CLI入口)调用入站端口;被驱动型适配器(VTK渲染、DICOM IO、Boost.Asio设备通信、文件系统)实现出站端口。
依赖规则:适配器 → 端口 → 领域;领域不包含任何#include <vtk...>或#include <boost/asio...>。
3.2 端口清单(节选)
| 端口 | 方向 | 职责 |
|---|---|---|
IPlanningUseCase | 入站 | 规划会话的创建、编辑、提交 |
INavigationUseCase | 入站 | 术中导航会话控制 |
IImagingRepository | 出站 | 体数据/网格的加载与持久化 |
IVolumeRenderer | 出站 | 体绘制与网格叠加渲染 |
ITrackerDevice | 出站 | 器械位姿流订阅 |
IAuditLogger | 出站 | 合规审计事件 |
IClock | 出站 | 时间抽象(便于测试) |
3.3 模块与构建视图
采用CMake多目标组织,以编译期强制依赖规则:
text
dipsas_domain (STATIC, 无VTK/Boost.Asio依赖) dipsas_ports (INTERFACE, 仅头文件) dipsas_adapter_vtk (STATIC, 链接VTK) dipsas_adapter_io (STATIC, 链接Boost.Asio/DCMTK) dipsas_app (EXECUTABLE, 组合根) dipsas_tests (测试目标, 链接domain+ports+fake适配器)
dipsas_domain的CMake中显式不声明VTK依赖,任何误引入将导致链接失败,从构建系统层面固化架构约束。
4 领域核心与端口设计
4.1 领域模型(值对象与实体)
cpp
// domain/geometry_types.hpp —— 纯标准库,替代vtkVector
namespace dipsas::domain {
struct Vec3 { double x{}, y{}, z{}; };
struct Mat4 { double m[16]{}; }; // 列主序
struct Transform { Mat4 matrix; };
struct ImplantPlan {
std::string toothFdi; // FDI牙位编号
Vec3 entryPoint;
Vec3 apexPoint;
double diameterMm{};
double lengthMm{};
};
struct RegistrationResult {
Transform preOpToTracker;
double fiducialRmsMm{};
bool converged{};
};
} // namespace
4.2 出站端口
cpp
// ports/outbound/volume_renderer.hpp
namespace dipsas::ports {
class IVolumeRenderer {
public:
virtual ~IVolumeRenderer() = default;
virtual void loadVolume(const domain::VolumeId& id) = 0;
virtual void overlayMesh(const domain::MeshId& id,
const domain::Transform& xform) = 0;
virtual void setImplantPreview(const domain::ImplantPlan& plan) = 0;
virtual void requestRender() = 0; // 线程语义由适配器文档约定
};
class ITrackerDevice {
public:
struct PoseSample {
domain::Transform toolToTracker;
std::chrono::steady_clock::time_point stamp;
};
using Callback = std::function<void(const PoseSample&)>;
virtual ~ITrackerDevice() = default;
virtual void start(Callback cb) = 0;
virtual void stop() = 0;
};
} // namespace
端口设计要点:参数与返回值仅使用领域类型与标准库类型,杜绝VTK/Boost类型泄漏;线程与生命周期语义以契约注释形式固化,避免"接口能编译但语义不明"。
4.3 入站端口与用例实现
cpp
// ports/inbound/planning_use_case.hpp
namespace dipsas::ports {
class IPlanningUseCase {
public:
virtual ~IPlanningUseCase() = default;
virtual domain::PlanId createPlan(const domain::PatientId&) = 0;
virtual void addImplant(const domain::PlanId&, domain::ImplantPlan) = 0;
virtual domain::RegistrationResult registerFiducials(
const domain::PlanId&, const std::vector<domain::Vec3>& pre,
const std::vector<domain::Vec3>& intra) = 0;
virtual void commit(const domain::PlanId&) = 0;
};
}
cpp
// domain/planning_service.cpp —— 用例编排,注入出站端口
namespace dipsas::domain {
class PlanningService final : public ports::IPlanningUseCase {
public:
PlanningService(std::shared_ptr<ports::IImagingRepository> repo,
std::shared_ptr<ports::IAuditLogger> audit,
std::shared_ptr<ports::IClock> clock)
: repo_(std::move(repo)), audit_(std::move(audit)), clock_(std::move(clock)) {}
PlanId createPlan(const PatientId& pid) override {
PlanId id = repo_->createPlan(pid);
audit_->log({clock_->now(), "PLAN_CREATED", pid.value});
return id;
}
RegistrationResult registerFiducials(const PlanId& id,
const std::vector<Vec3>& pre, const std::vector<Vec3>& intra) override {
// 领域算法:SVD-based 点集配准(纯计算,可单测)
auto result = computeRigidRegistration(pre, intra);
audit_->log({clock_->now(), "REGISTRATION_DONE", id.value,
std::to_string(result.fiducialRmsMm)});
return result;
}
// ...
private:
std::shared_ptr<ports::IImagingRepository> repo_;
std::shared_ptr<ports::IAuditLogger> audit_;
std::shared_ptr<ports::IClock> clock_;
};
}
配准算法computeRigidRegistration为纯函数,CI中无需任何图形或设备即可验证,直接满足FR2的可测试性诉求。
5 适配器实现
5.1 VTK渲染适配器
cpp
// adapters/vtk/vtk_volume_renderer.hpp
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkVolume.h>
#include <vtkActor.h>
#include "ports/outbound/volume_renderer.hpp"
namespace dipsas::adapters::vtk {
class VtkVolumeRenderer final : public ports::IVolumeRenderer {
public:
explicit VtkVolumeRenderer(vtkSmartPointer<vtkRenderer> renderer)
: renderer_(std::move(renderer)) {}
void loadVolume(const domain::VolumeId& id) override {
auto reader = makeDicomReader(id); // 内部封装DCMTK/VTK
auto mapper = vtkSmartPointer<vtkGPUVolumeRayCastMapper>::New();
mapper->SetInputConnection(reader->GetOutputPort());
volume_ = vtkSmartPointer<vtkVolume>::New();
volume_->SetMapper(mapper);
renderer_->AddVolume(volume_);
}
void overlayMesh(const domain::MeshId& id,
const domain::Transform& x) override {
auto actor = meshCache_.getOrLoad(id); // 网格缓存
actor->SetUserMatrix(toVtkMatrix(x)); // 领域Mat4 -> vtkMatrix4x4
renderer_->AddActor(actor);
}
void requestRender() override { renderer_->GetRenderWindow()->Render(); }
// ...
private:
vtkSmartPointer<vtkRenderer> renderer_;
vtkSmartPointer<vtkVolume> volume_;
MeshActorCache meshCache_;
};
}
适配器是唯一允许出现VTK类型的编译单元;领域Transform到vtkMatrix4x4的转换被限制在适配器内部的toVtkMatrix函数中,形成明确的"翻译边界"。
5.2 Boost.Asio跟踪设备适配器
cpp
// adapters/io/asio_tracker_device.hpp
#include <boost/asio.hpp>
#include "ports/outbound/tracker_device.hpp"
namespace dipsas::adapters::io {
class AsioTrackerDevice final : public ports::ITrackerDevice {
public:
AsioTrackerDevice(boost::asio::io_context& io, std::string endpoint)
: socket_(io), endpoint_(std::move(endpoint)) {}
void start(Callback cb) override {
callback_ = std::move(cb);
socket_.connect(udp::endpoint(
boost::asio::ip::make_address("0.0.0.0"), portOf(endpoint_)));
doReceive();
}
void stop() override { socket_.close(); }
private:
void doReceive() {
socket_.async_receive(boost::asio::buffer(buf_),
[this](auto ec, std::size_t n) {
if (ec) return;
if (auto s = parsePose(buf_, n)) callback_(*s);
doReceive();
});
}
// parsePose: 字节流 -> domain::Transform(适配器内翻译)
udp::socket socket_;
std::array<std::byte, 256> buf_{};
Callback callback_;
};
}
Boost.Asio的异步模型被完全封装在适配器内,领域层仅感知PoseSample回调。若未来替换为同步串口或厂商SDK,仅需新增一个ITrackerDevice实现,组合根更换注入即可,满足2.2节的可替换性场景。
5.3 组合根与依赖注入
cpp
// app/main.cpp
int main(int argc, char** argv) {
boost::asio::io_context io;
auto audit = std::make_shared<FileAuditLogger>("audit.log");
auto clock = std::make_shared<SystemClock>();
auto repo = std::make_shared<DicomImagingRepository>();
auto useCase = std::make_shared<domain::PlanningService>(repo, audit, clock);
QApplication app(argc, argv);
auto renderer = std::make_shared<adapters::vtk::VtkVolumeRenderer>(/*...*/);
auto tracker = std::make_shared<adapters::io::AsioTrackerDevice>(io, "0.0.0.0:9000");
MainWindow w(useCase, renderer, tracker); // 驱动型适配器
w.show();
std::thread ioThread([&]{ io.run(); });
return app.exec();
}
组合根是唯一知晓全部具体类型的位置,依赖注入以构造函数完成,避免服务定位器带来的隐式依赖。
6 架构属性分析
6.1 可测试性
领域与端口目标不链接VTK/Asio,CI中可直接以FakeRenderer、FakeTracker、ManualClock驱动用例测试。配准、审计事件时序、规划状态机均可确定性验证;测试目标构建时间与GPU无关,符合IEC 62304对单元验证的要求。
6.2 可替换性与可演进性
外部技术栈变更被限制在适配器:VTK 9→10升级只影响adapter_vtk;跟踪设备更换仅新增实现类。端口作为稳定契约,使需求-代码-测试的追溯链在技术演进中保持稳定。
6.3 性能考量
六边形架构的常见质疑是抽象层开销。分析表明:端口调用为虚函数或std::function间接调用,相对VTK渲染管线与DICOM解码的毫秒级开销可忽略;体绘制热点在适配器内部,可直接使用VTK原生管线与GPU加速;Boost并行算法(如boost::sort、boost::geometry批量运算)在适配器或领域纯计算函数中均可使用,唯一约束是领域不得引入带全局状态的Boost组件(如Asio的io_context),而boost::geometry的纯算法部分经评估可在领域层受限使用,并在编码规范中列明白名单。
6.4 代价与约束
-
类型翻译成本:领域类型与VTK/协议字节流之间需显式转换,增加适配器代码量;以集中式转换工具与契约测试缓解。
-
端口粒度设计难度:端口过细则适配器爆炸,过粗则泄漏技术细节;本项目以用例为粒度、以"一次外部交互"为出站端口边界。
-
团队认知负担:需通过构建系统强制依赖规则、代码评审检查
#include白名单来维持架构不腐化。
7 相关工作与讨论
六边形架构与Clean Architecture[2]、洋葱架构同属依赖倒置家族;在C++领域,其落地关键在于编译期约束而非仅靠约定。本文以CMake目标隔离替代"架构守护测试",成本更低且更严格。与Martin的Clean Architecture相比,本文未强制引入UseCase/Interactor双层,而是以入站端口直接承载用例,减少医疗小团队的样板代码。局限在于:实时导航路径中跨线程的端口调用需明确线程契约,本文以接口注释与适配器内部串行化处理,尚未引入形式化验证;未来可结合Boost.Asio的executor模型为端口调用定义统一的执行上下文语义。
8 结论
本文面向牙科影像处理与手术辅助系统,提出并实现了基于C++17/VTK/Boost的六边形架构:以领域核心为中心,入站端口承载用例,出站端口抽象影像仓库、渲染、设备与审计能力,适配器隔离全部外部技术栈,组合根完成依赖注入。构建系统层面的目标隔离将依赖规则固化为编译约束。分析表明,该架构在保持VTK渲染性能与Boost异步/并行能力的同时,显著提升了领域逻辑的可测试性与技术栈可替换性,契合医疗软件长生命周期与合规验证需求。后续工作将聚焦端口线程语义的形式化、适配器契约测试自动化,以及在多模态配准流水线中的并行调度优化。
参考文献
[1] Cockburn A. Hexagonal Architecture (Ports and Adapters). 2005.
[2] Martin R C. Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall, 2017.
[3] Gamma E, Helm R, Johnson R, Vlissides J. Design Patterns. Addison-Wesley, 1994.
[4] Schroeder W, Martin K, Lorensen B. The Visualization Toolkit (4th ed.). Kitware, 2006.
[5] Schlingensiepen J, et al. Boost C++ Libraries 文档. boost.org.
[6] IEC 62304:2006/AMD1:2015 Medical device software — Software life cycle processes.
[7] Fowler M. Patterns of Enterprise Application Architecture. Addison-Wesley, 2002.(依赖倒置与网关模式)
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)