1背景
上一篇本来就想写这个读取的,想起来先看下vtk是否能正常显示,结果出了一堆意外。这一篇开始填充之前的框架。
2正文
2.1 模型
2.1.1 Frame
存储图像数据用gdcm,存储tag等用dcmtk,后面用混合的组成Frame
c
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
class Frame
{
public:
explicit Frame();
~Frame() = default;
private:
std::vector<uint16_t> pixels;
int rows;
int cols;
double pixelSpacingX;
double pixelSpacingY;
std::array<double, 3> imagePositionPatient;
std::array<double, 6> imageOrientationPatient;
std::string sopInstanceUid;
int frameIndex; // 多帧中的编号
double slope;
double intercept;
public:
double getSlope() const;
void setSlope(double slope);
double getIntercept() const;
void setIntercept(double intercept);
int getFrameIndex() const;
void setFrameIndex(int frameIndex);
const std::vector<uint16_t>& getPixels() const;
void setPixels(const std::vector<uint16_t>& pixels);
int getRows() const;
void setRows(int rows);
int getCols() const;
void setCols(int cols);
double getPixelSpacingX() const;
void setPixelSpacingX(double pixelSpacingX);
double getPixelSpacingY() const;
void setPixelSpacingY(double pixelSpacingY);
const std::array<double, 3>& getImagePositionPatient() const;
void setImagePositionPatient(const std::array<double, 3>& imagePositionPatient);
const std::array<double, 6>& getImageOrientationPatient() const;
void setImageOrientationPatient(const std::array<double, 6>& imageOrientationPatient);
const std::string& getSopInstanceUid() const;
void setSopInstanceUid(const std::string& sopInstanceUid);
};

2.1.2 DisplaySettings
窗宽窗位,这里暂时先用写死的,后面再从tag里读取
c++
#pragma once
class DisplaySettings
{
public:
explicit DisplaySettings();
~DisplaySettings()=default;
private:
double windowWidth=400;
double windowCenter=40;
public:
// --- Getters & Setters (auto-generated) ---
double getWindowWidth() const;
void setWindowWidth(double windowWidth);
double getWindowCenter() const;
void setWindowCenter(double windowCenter);
};
2.1.3 StackDisplaySet
这个存储读取的dcm的所有帧或者vtkImageData的实例id,以及当前显示的索引,还有上面当前的窗宽窗位
c++
#pragma once
#include <vector>
#include <string>
#include "DisplaySettings.h"
class StackDisplaySet
{
public:
explicit StackDisplaySet();
~StackDisplaySet()=default;
public:
private:
std::vector<std::string> frameUids;
int currentIndex;
DisplaySettings displaySettings;
public:
// --- Getters & Setters (auto-generated) ---
int getCurrentIndex() const;
void setCurrentIndex(int currentIndex);
const std::vector<std::string>& getFrameUids() const;
void setFrameUids(const std::vector<std::string>& frameUids);
const DisplaySettings& getDisplaySettings() const;
void setDisplaySettings(const DisplaySettings& displaySettings);
};
2.2 读取模型
2.2.1 模型缓存接口
主要是兼容Frame,vtkImageData,以及空指针
c++
#pragma once
#include <memory>
#include "domain/model/Frame.h"
#include <variant>
#include "vtkSmartPointer.h"
#include "vtkImageData.h"
class IFrameCache
{
public:
explicit IFrameCache();
~IFrameCache()=default;
public:
using FramePtr = std::variant<std::shared_ptr<Frame>, vtkSmartPointer<vtkImageData>,nullptr_t>;
virtual void put(const std::string& uid, int index, const IFrameCache::FramePtr& frame) = 0;
virtual IFrameCache::FramePtr get(const std::string& uid, int index) = 0;
};
2.2.2 通用读取接口类
一开始直接读取没有参考的可能没法正常显示,所以考虑需要增加vtkImageData来对比。
c++
#pragma once
#include <string>
#include <memory>
#include "domain/model/Series.h"
#include "domain/model/Frame.h"
#include "infrastructure/cache/IFrameCache.h"
class IDicomReader
{
public:
explicit IDicomReader();
~IDicomReader()=default;
virtual void open(const std::string& filePath) = 0;
virtual Series readSeries(const std::string& path) = 0;
virtual std::unique_ptr<Frame> readFrameInfo( int index) = 0;
virtual IFrameCache::FramePtr readFrame(int index) = 0;
virtual void close() = 0;
};
2.2.3 dcmtk只读取文件信息
先考虑单帧的,只处理readFrameInfo,后面再解析多帧的,或者直接用vtkDicomReader
c++
#include "DcmtkReader.h"
#include <dcmtk/dcmdata/dcdeftag.h>
#include <dcmtk/dcmdata/dcvrat.h>
#include <dcmtk/dcmfg/fgbase.h>
#include <dcmtk/dcmfg/fginterface.h>
#include <dcmtk/dcmfg/fgtypes.h>
DcmtkReader::DcmtkReader() {}
void DcmtkReader::open(const std::string& path)
{
m_fileFormat = std::make_unique<DcmFileFormat>();
if (m_fileFormat->loadFile(path.c_str()).bad()) {
// 处理加载失败的情况,例如抛出异常或记录错误日志
throw std::runtime_error("Failed to load DICOM file: " + path);
} else {
m_dataset = std::make_unique<DcmDataset>(*m_fileFormat->getDataset());
}
}
Series DcmtkReader::readSeries(const std::string& path)
{
return Series();
}
template<size_t N>
bool getTagAsArray(const std::unique_ptr<DcmDataset>& dataset,
const DcmTagKey& tag,
std::array<double, N>& arr)
{
bool allGood = true;
for (size_t i = 0; i < N; ++i) {
OFCondition status = dataset->findAndGetFloat64(tag, arr[i], i);
if (!status.good()) {
allGood = false;
// 可以选择break或继续
throw std::runtime_error("Failed to read DICOM tag: " + std::string(status.text()));
}
}
return allGood;
}
std::unique_ptr<Frame> DcmtkReader::readFrameInfo(int index)
{
auto frame =std::make_unique<Frame>();
OFString sopInstanceUid;
m_dataset->findAndGetOFString(DCM_SOPInstanceUID, sopInstanceUid);
frame->setSopInstanceUid(std::string(sopInstanceUid.c_str()));
OFString numberOfFramesStr;
int numberOfFrames = 1;
if (m_dataset->findAndGetOFString(DCM_NumberOfFrames, numberOfFramesStr)
.good())
{
numberOfFrames = std::atoi(numberOfFramesStr.c_str());
}
//更新索引
frame->setFrameIndex(index);
bool isSingleFrame = (numberOfFrames == 1);
bool isEnhancedMultiFrame = false; // 这里可以添加逻辑来判断是否为增强型多帧DICOM
bool isOldMultiFrame = false; // 这里可以添加逻辑来判断是否为旧式多帧DICOM
if (!isSingleFrame) {
FGInterface fgInterface;
if (m_dataset) {
if (fgInterface.read(*m_dataset).good()) {
isEnhancedMultiFrame = true;
} else {
isOldMultiFrame = true;
}
}
}
//先处理单帧
if (isSingleFrame) {
// 像素间距 先行 后列
std::array<double, 2> pixelSpacing = {0.0, 0.0};
if (getTagAsArray(m_dataset, DCM_PixelSpacing, pixelSpacing)) {
frame->setPixelSpacingY(pixelSpacing[0]);
frame->setPixelSpacingX(pixelSpacing[1]);
}
// 图像第一个像素的3D世界坐标
std::array<double, 3> position = {0.0, 0.0, 0.0};
if (getTagAsArray(m_dataset, DCM_ImagePositionPatient, position)) {
frame->setImagePositionPatient(position);
}
// 图像行和列方向的方向余弦,包含6个值。
std::array<double, 6> orientation = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
if (getTagAsArray(m_dataset, DCM_ImageOrientationPatient, orientation)) {
frame->setImageOrientationPatient(orientation);
}
// 斜率和截距
double slope = 1.0;
double intercept = 0.0;
OFCondition status = m_dataset->findAndGetFloat64(DCM_RescaleSlope, slope, 0);
if(!status.good()) {
slope = 1.0; // 默认值
}
status = m_dataset->findAndGetFloat64(DCM_RescaleIntercept, intercept, 0);
if(!status.good()) {
intercept = 0.0; // 默认值
}
frame->setSlope(slope);
frame->setIntercept(intercept);
}
return frame;
}
IFrameCache::FramePtr DcmtkReader::readFrame(int index)
{
return IFrameCache::FramePtr();
}
void DcmtkReader::close() {}
2.2.4 gdcm读取图像数据
主要实现readFrame
c++
#include "GdcmReader.h"
#include <gdcmBoxRegion.h>
#include <gdcmImage.h>
#include <gdcmImageHelper.h>
#include <gdcmPixmap.h>
#include <gdcmPixelFormat.h>
GdcmReader::GdcmReader() {}
void GdcmReader::open(const std::string& path)
{
m_image_reader = std::make_unique<gdcm::ImageReader>();
m_image_reader->SetFileName(path.c_str());
filePath=path.c_str();
if (!m_image_reader->Read()) {
throw std::runtime_error("Failed to read DICOM file: " + path);
}
}
Series GdcmReader::readSeries(const std::string& path)
{
return Series();
}
IFrameCache::FramePtr GdcmReader::readFrame(int index)
{
if(!m_image_reader) {
throw std::runtime_error("DICOM file not opened. Call open() first.");
}
if(!filePath) {
throw std::runtime_error("File path is not set. Call open() first.");
}
m_image_reader->SetFileName(filePath);
if(!m_image_reader->Read()) {
throw std::runtime_error("Failed to read DICOM file: " + std::string(filePath));
}
auto image = m_image_reader->GetImage();
auto dims = image.GetDimensions();
auto numFrames = (image.GetNumberOfDimensions() == 3) ? dims[2] : 1;
if (index < 0 || index >= static_cast<int>(numFrames)) {
throw std::out_of_range("Frame index out of range");
}
unsigned int rows = image.GetRows();
unsigned int cols = image.GetColumns();
std::vector<uint16_t> pixels = extractPixelData(image, index);
auto frame = std::make_unique<Frame>();
frame->setPixels(pixels);
frame->setRows(rows);
frame->setCols(cols);
return frame;
}
void GdcmReader::close() {}
template<typename T>
std::vector<uint16_t> convertPixels(const char* buffer, size_t count)
{
const T* src = reinterpret_cast<const T*>(buffer);
std::vector<uint16_t> pixels(count);
for (size_t i = 0; i < count; ++i)
{
if constexpr (std::is_same_v<T, int8_t>)
{
// [-128,127] → [0,255]
pixels[i] = static_cast<uint16_t>(
static_cast<int16_t>(src[i]) + 128
);
}
else if constexpr (std::is_same_v<T, int16_t>)
{
// 直接按位 reinterpret(DICOM 正确做法)
pixels[i] = static_cast<uint16_t>(src[i]);
}
else if constexpr (std::is_same_v<T, uint8_t>)
{
// 8-bit → 16-bit(无符号扩展)
pixels[i] = static_cast<uint16_t>(src[i]) * 257u;
}
else if constexpr (std::is_same_v<T, uint16_t>)
{
pixels[i] = src[i];
}
else if constexpr (std::is_floating_point_v<T>)
{
// 假设 float 在 [0,1]
constexpr double scale = 65535.0;
double v = src[i];
v = std::clamp(v, 0.0, 1.0);
pixels[i] = static_cast<uint16_t>(v * scale + 0.5);
}
else
{
static_assert(!sizeof(T), "Unsupported pixel type");
}
}
return pixels;
}
// 运行时分派:枚举 -> 调用对应模板实例
std::vector<uint16_t> GdcmReader::extractPixelData(const gdcm::Image& image, int frameIndex) {
const gdcm::PixelFormat& pf = image.GetPixelFormat();
unsigned int rows = image.GetRows();
unsigned int cols = image.GetColumns();
size_t count = rows * cols;
// 计算帧偏移
const unsigned int* dims = image.GetDimensions();
unsigned int numFrames = (image.GetNumberOfDimensions() == 3) ? dims[2] : 1;
size_t frameSizeInBytes = image.GetBufferLength() / numFrames;
unsigned long bufLen = image.GetBufferLength();
std::vector<char> fullBuffer(bufLen);
image.GetBuffer(fullBuffer.data());
const char* buffer = fullBuffer.data() + frameIndex * frameSizeInBytes;
switch (pf.GetScalarType()) {
case gdcm::PixelFormat::UINT8: return convertPixels<uint8_t>(buffer, count);
case gdcm::PixelFormat::INT8: return convertPixels<int8_t>(buffer, count);
case gdcm::PixelFormat::UINT16: return convertPixels<uint16_t>(buffer, count);
case gdcm::PixelFormat::INT16: return convertPixels<int16_t>(buffer, count);
default: throw std::runtime_error("Unsupported pixel type");
}
}
std::unique_ptr<Frame> GdcmReader::readFrameInfo(int index)
{
return std::unique_ptr<Frame>(new Frame());
}
2.2.5 HybridReader组合dcmtk和gdcm为Frame
还是只实现readFrame
c++
#include "HybridReader.h"
#include "DcmtkReader.h"
#include "GdcmReader.h"
HybridReader::HybridReader()
{
m_dcmtk_reader = std::make_unique<DcmtkReader>();
m_gdcm_reader = std::make_unique<GdcmReader>();
}
void HybridReader::open(const std::string& filePath) {
m_dcmtk_reader->open(filePath);
m_gdcm_reader->open(filePath);
}
Series HybridReader::readSeries(const std::string& path)
{
return Series();
}
std::unique_ptr<Frame> HybridReader::readFrameInfo(int index)
{
return std::unique_ptr<Frame>(new Frame());
}
IFrameCache::FramePtr HybridReader::readFrame(int index)
{
const auto frameInfo = m_dcmtk_reader->readFrameInfo(index);
auto frameData = m_gdcm_reader->readFrame(index);
std::visit([&frameInfo](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
arg->setPixelSpacingX(frameInfo->getPixelSpacingX());
arg->setPixelSpacingY(frameInfo->getPixelSpacingY());
arg->setImagePositionPatient(frameInfo->getImagePositionPatient());
arg->setImageOrientationPatient(frameInfo->getImageOrientationPatient());
arg->setSopInstanceUid(frameInfo->getSopInstanceUid());
arg->setFrameIndex(frameInfo->getFrameIndex());
arg->setSlope(frameInfo->getSlope());
arg->setIntercept(frameInfo->getIntercept());
}
}, frameData);
return frameData;
}
void HybridReader::close() {}
2.2.6 VTKDicomAdaptReader直接读取vtkImageData
这里需要有实例id,因为要从缓存读取
c++
#include "VTKDicomAdaptReader.h"
#include <vtkDICOMMetaData.h>
#include <vtkDICOMTagPath.h>
#include <vtkFieldData.h>
#include <vtkImageData.h>
#include <vtkStringArray.h>
VTKDicomAdaptReader::VTKDicomAdaptReader()
{
m_reader = vtkSmartPointer<vtkDICOMReader>::New();
}
void VTKDicomAdaptReader::open(const std::string& filePath)
{
m_filePath = filePath;
m_reader->SetFileName(m_filePath.c_str());
m_reader->Update();
}
Series VTKDicomAdaptReader::readSeries(const std::string& path)
{
return Series();
}
std::unique_ptr<Frame> VTKDicomAdaptReader::readFrameInfo(int index)
{
return std::unique_ptr<Frame>();
}
IFrameCache::FramePtr VTKDicomAdaptReader::readFrame(int index)
{
if (!m_reader) {
return nullptr;
}
if (!m_reader->GetOutput()) {
return nullptr;
}
auto imageData = m_reader->GetOutput();
vtkDICOMMetaData* meta = m_reader->GetMetaData();
std::string sopUID =
meta->GetAttributeValue(0, DC::SOPInstanceUID).AsString();
vtkSmartPointer<vtkStringArray> uidArray =
vtkSmartPointer<vtkStringArray>::New();
uidArray->SetName("SOPInstanceUIDs");
uidArray->InsertNextValue(sopUID);
imageData->GetFieldData()->AddArray(uidArray);
return imageData;
}
void VTKDicomAdaptReader::close() {}
2.3 缓存
2.3.1 缓存接口
之前的读取模型里因为用了,所以放到2.2.1
2.3.2 缓存实现
头文件
c++
#pragma once
#include "IFrameCache.h"
#include <unordered_map>
class MemoryFrameCache : public IFrameCache
{
public:
explicit MemoryFrameCache();
~MemoryFrameCache()=default;
public:
void put(const std::string& uid, int index, const IFrameCache::FramePtr& frame) override;
IFrameCache::FramePtr get(const std::string& uid, int index) override;
private:
std::unordered_map<std::string, std::unordered_map<int, IFrameCache::FramePtr>> cache;
};
c++
#include "MemoryFrameCache.h"
MemoryFrameCache::MemoryFrameCache()
{
cache=std::unordered_map<std::string, std::unordered_map<int, IFrameCache::FramePtr>>();
}
void MemoryFrameCache::put(const std::string& uid,
int index,
const IFrameCache::FramePtr& frame)
{
if (cache.find(uid) == cache.end())
{
cache[uid] = std::unordered_map<int, IFrameCache::FramePtr>();
}
cache[uid][index] = frame;
}
IFrameCache::FramePtr MemoryFrameCache::get(const std::string& uid, int index)
{
if (cache.find(uid) != cache.end())
{
const auto& frameMap = cache[uid];
if (frameMap.find(index) != frameMap.end())
{
return frameMap.at(index);
}
}
return IFrameCache::FramePtr();
}
2.4 解析下载队列
2.4.1 队列接口
c++
#pragma once
#include <future>
#include <functional>
class ITaskQueue
{
public:
explicit ITaskQueue();
~ITaskQueue()=default;
public:
virtual std::future<void> submit(std::function<void()> task,
int priority = 0) = 0;
template<typename T>
auto submitWithResult(T&& f, int priority = 0) -> std::future<decltype(f())>
{
auto promise = std::make_shared<std::promise<decltype(f())>>();
auto future = promise->get_future();
submit(
[promise, f = std::forward<T>(f)]()
{
try {
promise->set_value(f());
} catch (...) {
promise->set_exception(std::current_exception());
}
},
priority);
return future;
}
virtual void cancelAll() = 0;
};
2.4.2 基于qt线程池实现
c++
#pragma once
#include "ITaskQueue.h"
#include <QThreadPool>
class QtTaskQueue : public ITaskQueue
{
public:
explicit QtTaskQueue( QThreadPool* threadPool = nullptr);
~QtTaskQueue()=default;
private:
QThreadPool* m_threadPool;
public:
std::future<void> submit(std::function<void()> task, int priority = 0) override;
void cancelAll() override;
};
arduino
#include "QtTaskQueue.h"
QtTaskQueue::QtTaskQueue(QThreadPool* threadPool)
: m_threadPool(threadPool ? threadPool : QThreadPool::globalInstance())
{
}
std::future<void> QtTaskQueue::submit(std::function<void()> task, int priority)
{
auto promise = std::make_shared<std::promise<void>>();
auto future = promise->get_future();
auto runnable = [promise, task = std::move(task)]()
{
try {
task();
promise->set_value();
} catch (...) {
promise->set_exception(std::current_exception());
}
};
struct TaskRunable : public QRunnable
{
std::function<void()> m_task;
void run() override { m_task(); }
};
auto qrunnable = new TaskRunable();
qrunnable->m_task = std::move(runnable);
qrunnable->setAutoDelete(true);
m_threadPool->start(qrunnable, priority);
return future;
}
void QtTaskQueue::cancelAll()
{
m_threadPool->clear();
}
2.5 渲染
2.5.1 渲染接口
c++
#pragma once
#include <memory>
#include "infrastructure/cache/IFrameCache.h"
#include "vtkRenderWindow.h"
#include "vtkSmartPointer.h"
#include "domain/model/DisplaySettings.h"
class IImageRenderer
{
public:
explicit IImageRenderer();
~IImageRenderer()=default;
virtual void setRenderTarget(vtkSmartPointer<vtkRenderWindow> window) = 0; // 设置渲染目标
virtual void render(const IFrameCache::FramePtr& frame,
const DisplaySettings& settings) = 0;
virtual void reset() = 0;
};
2.5.2 渲染2d
c++
#pragma once
#include "IImageRenderer.h"
#include "vtkSmartPointer.h"
#include "vtkImageActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkImageProperty.h"
#include "vtkImageMapToWindowLevelColors.h"
#include "vtkInteractorStyleImage.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkImageMapper3D.h"
class VtkRenderer : public IImageRenderer
{
public:
explicit VtkRenderer();
~VtkRenderer() = default;
public:
void setRenderTarget(vtkSmartPointer<vtkRenderWindow> window) override; // 设置渲染目标
void render(const IFrameCache::FramePtr& frame,
const DisplaySettings& settings) override;
void reset() override;
void CompareImageData(vtkImageData* img1, vtkImageData* img2);
vtkSmartPointer<vtkImageData> convertFrameToImageData(
const std::shared_ptr<Frame>& frame);
private:
vtkSmartPointer<vtkImageActor> m_imageActor;
vtkSmartPointer<vtkRenderer> m_renderer;
vtkSmartPointer<vtkRenderWindow> m_renderWindow;
vtkSmartPointer<vtkImageProperty> imageProperty;
vtkSmartPointer<vtkImageMapToWindowLevelColors> m_windowLevelColors;
vtkSmartPointer<vtkInteractorStyleImage> style;
vtkSmartPointer<vtkRenderWindowInteractor> interactor;
};
这里比较杂,一开始没显示出来,然后在Frame中嵌入vtkImageData,挨个对比不一样的才处理完
-
gdcm直接读取的要翻转坐标轴(坐标系不一样,要翻转y轴)
-
vtkImageData存储的目前这里是写死的,实际要根据tag来选择对应的存储,默认是double存储的
-
ct的数据要处理Slope和Intercept
-
Dimension 存储比较坑,第一个参数是列
-
方向的矩阵 还是因为坐标系,要转换
-
窗宽和窗位 一开始是用了过滤器,后面发现属性有就直接用属性了。能省点性能
c++
#include "VtkRenderer.h"
#include <vtkActor.h>
#include <vtkConeSource.h>
#include <vtkImageActor.h>
#include <vtkImageFlip.h>
#include <vtkImageMapToWindowLevelColors.h>
#include <vtkMatrix3x3.h>
#include <vtkPolyDataMapper.h>
VtkRenderer::VtkRenderer()
{
m_imageActor = vtkSmartPointer<vtkImageActor>::New();
m_renderer = vtkSmartPointer<vtkRenderer>::New();
m_renderer->AddActor(m_imageActor);
imageProperty = vtkSmartPointer<vtkImageProperty>::New();
style = vtkSmartPointer<vtkInteractorStyleImage>::New();
m_imageActor->SetProperty(imageProperty);
m_windowLevelColors = vtkSmartPointer<vtkImageMapToWindowLevelColors>::New();
}
void VtkRenderer::setRenderTarget(vtkSmartPointer<vtkRenderWindow> window)
{
m_renderWindow = window;
m_renderWindow->AddRenderer(m_renderer);
}
void VtkRenderer::render(const IFrameCache::FramePtr& frame,
const DisplaySettings& settings)
{
std::visit(
[this, &settings](auto&& arg)
{
using T = std::decay_t<decltype(arg)>;
vtkSmartPointer<vtkImageData> imageData;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
imageData = convertFrameToImageData(arg);
}
if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
imageData = arg;
}
if (!imageData) {
std::cerr << "Error: imageData is null!" << std::endl;
return;
}
// m_windowLevelColors->SetInputData(imageData);
// m_imageActor->GetMapper()->SetInputConnection(
// m_windowLevelColors->GetOutputPort());
// m_windowLevelColors->SetWindow(settings.getWindowWidth());
// m_windowLevelColors->SetLevel(settings.getWindowCenter());
// m_windowLevelColors->Update();
m_imageActor->GetMapper()->SetInputData(imageData);
imageProperty->SetColorWindow(settings.getWindowWidth());
imageProperty->SetColorLevel(settings.getWindowCenter());
interactor = m_renderWindow->GetInteractor();
interactor->SetInteractorStyle(style);
interactor->Initialize();
m_renderWindow->Render();
},
frame);
}
void VtkRenderer::reset() {}
void VtkRenderer::CompareImageData(vtkImageData* img1, vtkImageData* img2)
{
// 检查维度和数据类型
int dims1[3], dims2[3];
img1->GetDimensions(dims1);
img2->GetDimensions(dims2);
if (dims1[0] != dims2[0] || dims1[1] != dims2[1] || dims1[2] != dims2[2]) {
std::cerr << "Dimensions do not match!" << std::endl;
return;
}
int scalarType1 = img1->GetScalarType();
int scalarType2 = img2->GetScalarType();
if (scalarType1 != scalarType2) {
std::cerr << "Scalar types differ!" << std::endl;
return;
}
// 获取像素指针(假设均为 unsigned short)
auto* ptr1 = static_cast<unsigned short*>(img1->GetScalarPointer());
auto* ptr2 = static_cast<unsigned short*>(img2->GetScalarPointer());
int totalPixels = dims1[0] * dims1[1] * dims1[2];
// 统计差异
double sumDiff = 0.0;
double maxDiff = 0.0;
int diffCount = 0;
int firstDiffIdx = -1;
unsigned short firstVal1 = 0, firstVal2 = 0;
for (int i = 0; i < totalPixels; ++i) {
unsigned short v1 = ptr1[i];
unsigned short v2 = ptr2[i];
double diff = std::abs(static_cast<double>(v1) - static_cast<double>(v2));
if (diff != 0.0) {
sumDiff += diff;
if (diff > maxDiff) {
maxDiff = diff;
}
diffCount++;
if (firstDiffIdx == -1) {
firstDiffIdx = i;
firstVal1 = v1;
firstVal2 = v2;
}
}
}
double meanDiff = (diffCount > 0) ? (sumDiff / diffCount) : 0.0;
std::cout << "=== Pixel Comparison ===" << std::endl;
std::cout << "Total pixels: " << totalPixels << std::endl;
std::cout << "Number of differing pixels: " << diffCount << std::endl;
std::cout << "Mean absolute difference: " << meanDiff << std::endl;
std::cout << "Max absolute difference: " << maxDiff << std::endl;
if (firstDiffIdx != -1) {
std::cout << "First diff at linear index " << firstDiffIdx << " (row "
<< firstDiffIdx / dims1[0] << ", col " << firstDiffIdx % dims1[0]
<< ")"
<< ": img1=" << firstVal1 << ", img2=" << firstVal2 << std::endl;
} else {
std::cout << "All pixels are identical (within numeric precision)."
<< std::endl;
}
}
vtkSmartPointer<vtkImageData> VtkRenderer::convertFrameToImageData(
const std::shared_ptr<Frame>& frame)
{
auto imageData = vtkSmartPointer<vtkImageData>::New();
imageData->SetDimensions(frame->getCols(), frame->getRows(), 1);
imageData->SetSpacing(
frame->getPixelSpacingX(), frame->getPixelSpacingY(), 1.0);
imageData->SetOrigin(frame->getImagePositionPatient().data());
vtkSmartPointer<vtkMatrix3x3> directionMatrix =
vtkSmartPointer<vtkMatrix3x3>::New();
double R[3][3];
double RX = frame->getImageOrientationPatient()[0];
double RY = frame->getImageOrientationPatient()[1];
double RZ = frame->getImageOrientationPatient()[2];
double CX = frame->getImageOrientationPatient()[3];
double CY = frame->getImageOrientationPatient()[4];
double CZ = frame->getImageOrientationPatient()[5];
R[0][0] = RX;
R[1][0] = RY;
R[2][0] = RZ;
R[0][1] = CX;
R[1][1] = CY;
R[2][1] = CZ;
R[0][2] = RY * CZ - RZ * CY;
R[1][2] = RZ * CX - RX * CZ;
R[2][2] = RX * CY - RY * CX;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
directionMatrix->SetElement(i, j, R[i][j]);
}
}
imageData->SetDirectionMatrix(directionMatrix);
imageData->SetExtent(0, frame->getCols() - 1, 0, frame->getRows() - 1, 0, 0);
imageData->AllocateScalars(VTK_SHORT, 1);
int numPixels = frame->getRows() * frame->getCols();
std::vector<int16_t> huData(numPixels);
for (int i = 0; i < numPixels; ++i) {
huData[i] = static_cast<int16_t>(frame->getPixels()[i] * frame->getSlope()
+ frame->getIntercept());
}
memcpy(imageData->GetScalarPointer(),
huData.data(),
huData.size() * sizeof(int16_t));
imageData->Modified();
vtkSmartPointer<vtkImageFlip> flipper = vtkSmartPointer<vtkImageFlip>::New();
flipper->SetInputData(imageData);
flipper->SetFilteredAxis(1); // 1 代表 Y 轴
flipper->Update();
vtkSmartPointer<vtkImageData> flippedImageData = flipper->GetOutput();
return flippedImageData;
}
2.6 业务实现
串联队列以及解析并缓存
2.6.1 LoadSeriesUseCase
c++
#pragma once
#include "infrastructure/dicom_io/IDicomReader.h"
#include "infrastructure/task/ITaskQueue.h"
#include "infrastructure/cache/IFrameCache.h"
#include <future>
#include "domain/model/StackDisplaySet.h"
class LoadSeriesUseCase
{
public:
explicit LoadSeriesUseCase();
~LoadSeriesUseCase()=default;
LoadSeriesUseCase(IDicomReader& dicomReader,
ITaskQueue& taskQueue,
IFrameCache& frameCache);
std::future<std::shared_ptr<StackDisplaySet>> loadSeriesAsync(const std::string& path);
private:
// Add private members and methods here
IDicomReader& m_dicomReader;
ITaskQueue& m_taskQueue;
IFrameCache& m_frameCache;
};
这里不关心是用什么实现的,只是基于接口处理解析并缓存
c++
#include "LoadSeriesUseCase.h"
#include <vtkFieldData.h>
#include <vtkImageData.h>
#include <vtkStringArray.h>
LoadSeriesUseCase::LoadSeriesUseCase(IDicomReader& dicomReader,
ITaskQueue& taskQueue,
IFrameCache& frameCache)
: m_dicomReader(dicomReader)
, m_taskQueue(taskQueue)
, m_frameCache(frameCache)
{
}
std::future<std::shared_ptr<StackDisplaySet>>
LoadSeriesUseCase::loadSeriesAsync(const std::string& path)
{
return m_taskQueue.submitWithResult(
[this, path]() -> std::shared_ptr<StackDisplaySet>
{
m_dicomReader.open(path);
std::vector<std::string> frameUids;
auto frame = m_dicomReader.readFrame(0);
std::visit(
[&](auto&& frameVariant)
{
using T = std::decay_t<decltype(frameVariant)>;
std::string sopUid;
int frameIndex = 0;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
if (frameVariant) {
sopUid = frameVariant->getSopInstanceUid();
frameIndex = frameVariant->getFrameIndex();
}
}
if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
if (frameVariant) {
vtkStringArray* uidArray = vtkStringArray::SafeDownCast(
frameVariant->GetFieldData()->GetAbstractArray(
"SOPInstanceUIDs"));
if (uidArray && uidArray->GetNumberOfValues() > 0) {
sopUid = uidArray->GetValue(0);
}
}
}
if (!sopUid.empty()) {
frameUids.push_back(sopUid);
m_frameCache.put(sopUid, frameIndex, frameVariant);
}
},
frame);
auto displaySet = std::make_shared<StackDisplaySet>();
displaySet->setFrameUids(frameUids);
displaySet->setCurrentIndex(0);
return displaySet;
});
}
2.7 模型与视图
界面调用这里方法的来实现
2.7.1 SeriesViewModel
加载完成的信号,以及具体要调用哪个解析来读取,调用业务层
c++
#pragma once
#include <QObject>
#include <memory>
#include <string>
#include <future>
#include "infrastructure/rendering/IImageRenderer.h"
#include "infrastructure/dicom_io/IDicomReader.h"
#include "application/LoadSeriesUseCase.h"
#include "infrastructure/task/ITaskQueue.h"
#include "infrastructure/cache/IFrameCache.h"
class SeriesViewModel : public QObject
{
Q_OBJECT
public:
explicit SeriesViewModel(QObject *parent = nullptr);
public:
void setRenderWindow(vtkSmartPointer<vtkRenderWindow> window);
void render();
void loadSeries(const std::string& path);
signals:
void imageChanged();
private:
std::shared_ptr<IImageRenderer> m_imageRenderer;
std::shared_ptr<StackDisplaySet> m_stackDisplaySet;
std::unique_ptr<IDicomReader> m_dicomReader;
std::unique_ptr<ITaskQueue> m_taskQueue;
std::unique_ptr<IFrameCache> m_frameCache;
std::unique_ptr<LoadSeriesUseCase> m_loadSeriesUseCase;
};
c++
#include "SeriesViewModel.h"
#include "infrastructure/rendering/VtkRenderer.h"
#include "infrastructure/dicom_io/HybridReader.h"
#include "infrastructure/dicom_io/VTKDicomAdaptReader.h"
#include "infrastructure/task/QtTaskQueue.h"
#include "infrastructure/cache/MemoryFrameCache.h"
SeriesViewModel::SeriesViewModel(QObject* parent)
: QObject(parent)
{
m_imageRenderer = std::make_shared<VtkRenderer>();
m_stackDisplaySet = std::make_shared<StackDisplaySet>();
m_dicomReader = std::make_unique<VTKDicomAdaptReader>();
m_taskQueue = std::make_unique<QtTaskQueue>();
m_frameCache = std::make_unique<MemoryFrameCache>();
m_loadSeriesUseCase = std::make_unique<LoadSeriesUseCase>(
*m_dicomReader, *m_taskQueue, *m_frameCache);
}
void SeriesViewModel::setRenderWindow(vtkSmartPointer<vtkRenderWindow> window)
{
m_imageRenderer->setRenderTarget(window);
}
void SeriesViewModel::render()
{
std::string sopUid = m_stackDisplaySet->getFrameUids().at(
m_stackDisplaySet->getCurrentIndex());
int frameIndex = m_stackDisplaySet->getCurrentIndex();
auto frame = m_frameCache->get(sopUid, frameIndex);
using FrameInfoType = std::decay_t<decltype(frame)>;
if constexpr (std::is_same_v<FrameInfoType, nullptr_t>) {
return;
}
m_imageRenderer->render(frame, m_stackDisplaySet->getDisplaySettings());
}
void SeriesViewModel::loadSeries(const std::string& path)
{
auto future = m_loadSeriesUseCase->loadSeriesAsync(path);
auto watcher =
std::make_shared<std::future<std::shared_ptr<StackDisplaySet>>>(
std::move(future));
std::thread(
[this, watcher]()
{
m_stackDisplaySet = watcher->get();
emit imageChanged();
})
.detach();
}
2.8 界面调用
2.8.1 GUICenter
- 主要是界面添加添加layout,加入QVTKOpenGLNativeWidget 实例。
- 然后打开菜单的选择文件,调用模型与视图的方法加载。
- 加载完成后,调用模型与视图的渲染。
- 渲染设置窗口,是等窗口加载后才设置的,防止渲染出问题
- 其他的是用来测试当前界面能正常显示的
arduino
#include <QFileDialog>
#include <memory>
#include "GUICenter.h"
#include <QVTKOpenGLNativeWidget.h>
#include "qdialog.h"
#include "qfiledialog.h"
#include "qtmetamacros.h"
#include "ui_GUICenter.h"
GUICenter::GUICenter(QWidget* parent)
: QWidget(parent),
ui(new Ui::GUICenter)
{
ui->setupUi(this);
//initvtkTest();
m_seriesViewModel = new SeriesViewModel(this);
m_vtkWidget = new QVTKOpenGLNativeWidget(this);
m_renderWindow = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
m_vtkWidget->setRenderWindow(m_renderWindow);
ui->layout->addWidget(m_vtkWidget);
connect(this, &GUICenter::addFiles, m_seriesViewModel, [this](const QStringList& paths) {
for (const auto& path : paths) {
m_seriesViewModel->loadSeries(path.toStdString());
}
});
connect(m_seriesViewModel, &SeriesViewModel::imageChanged, this, [this]() {
m_seriesViewModel->render();
});
}
void GUICenter::initvtkTest(){
// 1. 创建Qt控件
m_vtkWidget = new QVTKOpenGLNativeWidget(this);
// 2. 创建渲染窗口和渲染器
auto m_renderWindow = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
renderer = vtkSmartPointer<vtkRenderer>::New();
m_renderWindow->AddRenderer(renderer);
m_vtkWidget->setRenderWindow(m_renderWindow);
ui->layout->addWidget(m_vtkWidget);
}
GUICenter::~GUICenter()
{
}
void GUICenter::onOpenFile()
{
QFileDialog fileDialog(this, "选择文件");
fileDialog.setFileMode(QFileDialog::ExistingFile);
if (fileDialog.exec() == QDialog::Accepted) {
emit this->addFiles(fileDialog.selectedFiles());
}
}
void GUICenter::showEvent() {
m_seriesViewModel->setRenderWindow(m_renderWindow);
// auto source = vtkSmartPointer<vtkConeSource>::New();
// auto mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
// mapper->SetInputConnection(source->GetOutputPort());
// auto actor = vtkSmartPointer<vtkActor>::New();
// actor->SetMapper(mapper);
// renderer->AddActor(actor);
// renderer->ResetCamera();
// renderer->GetRenderWindow()->Render();
}
3 最终效果
3.1 混合读取(dcmtk读取tag,gdcm读取图像)--目前只处理了单帧

选择的是res下CT_small.dcm

3.2 使用vtk-dicom读取


3.3 单元测试
c++
#include <gtest/gtest.h>
#include "infrastructure/dicom_io/DcmtkReader.h" // 根据你的实际路径调整
#include "infrastructure/dicom_io/GdcmReader.h" // 根据你的实际路径调整
#include "infrastructure/dicom_io/HybridReader.h" // 根据你的实际路径调整
#include "infrastructure/dicom_io/VTKDicomAdaptReader.h" // 根据你的实际路径调整
#include "infrastructure/rendering/VtkRenderer.h" // 根据你的实际路径调整
#include "infrastructure/cache/MemoryFrameCache.h" // 根据你的实际路径调整
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
std::string single_dicom = "/home/ubuntu/xcyxiner/DicomViewer/res/CT_small.dcm"; // 准备一个真实的DICOM文件
std::string multiframe_dicom = "/home/ubuntu/xcyxiner/DicomViewer/res/enhanced_sample.dcm"; // 准备一个多帧的DICOM文件
TEST(DcmtkReaderTest, OpenValidFile) {
DcmtkReader reader;
EXPECT_NO_THROW(reader.open(single_dicom));
}
TEST(DcmtkReaderTest, readFrameInfo) {
DcmtkReader reader;
EXPECT_NO_THROW(reader.open(single_dicom));
EXPECT_NO_THROW(reader.readFrameInfo(0)); // 确保会抛出异常
auto frameInfo = reader.readFrameInfo(0);
EXPECT_NE(frameInfo, nullptr); // 确保返回的frameInfo不为空
}
TEST(GdcmReaderTest, OpenValidFile) {
GdcmReader reader;
EXPECT_NO_THROW(reader.open(single_dicom));
}
TEST(GdcmReaderTest, readFrame) {
GdcmReader reader;
reader.open(single_dicom);
EXPECT_NO_THROW(reader.readFrame(0)); // 确保不会抛出异常
auto frame = reader.readFrame(0);
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, std::nullptr_t>) {
FAIL() << "Frame is nullptr"; // 如果是nullptr,测试失败
}
}, frame);
}
TEST(HybridReaderTest, OpenValidFile) {
HybridReader reader;
EXPECT_NO_THROW(reader.open(single_dicom));
}
TEST(HybridReaderTest, readFrame) {
HybridReader reader;
reader.open(single_dicom);
EXPECT_NO_THROW(reader.readFrame(0)); // 确保不会抛出异常
auto frame = reader.readFrame(0);
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, std::nullptr_t>) {
FAIL() << "Frame is nullptr"; // 如果是nullptr,测试失败
}
}, frame);
}
TEST(VtkRendererTest,convertFrameToImageData) {
VtkRenderer renderer;
HybridReader reader;
reader.open(single_dicom);
auto frame = reader.readFrame(0);
std::visit([&renderer](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
auto imageData = renderer.convertFrameToImageData(arg);
EXPECT_NE(imageData, nullptr); // 确保返回的imageData不为空
} else if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(arg, nullptr); // 确保返回的imageData不为空
} else if constexpr (std::is_same_v<T, std::nullptr_t>) {
FAIL() << "Frame is nullptr"; // 如果是nullptr,测试失败
}
}, frame);
}
TEST(VtkRendererTest,CompareImageData) {
VtkRenderer renderer;
HybridReader reader;
VTKDicomAdaptReader vtkReader;
reader.open(single_dicom);
auto frame = reader.readFrame(0);
vtkReader.open(single_dicom);
auto vtkFrame = vtkReader.readFrame(0);
std::visit([&renderer,&vtkFrame](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
auto imageData = renderer.convertFrameToImageData(arg);
EXPECT_NE(imageData, nullptr); // 确保返回的imageData不为空
std::visit([&renderer, &imageData](auto&& vtkArg) {
using VTK_T = std::decay_t<decltype(vtkArg)>;
if constexpr (std::is_same_v<VTK_T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(vtkArg, nullptr); // 确保返回的vtkImageData不为空
renderer.CompareImageData(imageData, vtkArg);
} else if constexpr (std::is_same_v<VTK_T, std::nullptr_t>) {
FAIL() << "VTK Frame is nullptr"; // 如果是nullptr,测试失败
}
}, vtkFrame);
} else if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(arg, nullptr); // 确保返回的imageData不为空
} else if constexpr (std::is_same_v<T, std::nullptr_t>) {
FAIL() << "Frame is nullptr"; // 如果是nullptr,测试失败
}
}, frame);
}
TEST(MemoryFrameCacheTest, PutAndGetFrame) {
std::shared_ptr<MemoryFrameCache> cache = std::make_shared<MemoryFrameCache>();
std::string sopUid = "testSopUid";
int frameIndex = 0;
auto frame = std::make_shared<Frame>();
cache->put(sopUid, frameIndex, frame);
auto retrievedFrame = cache->get(sopUid, frameIndex);
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::shared_ptr<Frame>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, vtkSmartPointer<vtkImageData>>) {
EXPECT_NE(arg, nullptr); // 确保返回的frame不为空
} else if constexpr (std::is_same_v<T, std::nullptr_t>) {
FAIL() << "Frame is nullptr"; // 如果是nullptr,测试失败
}
}, retrievedFrame);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

3.4 git commit
