DicomViewer14(读取图像按固定窗宽窗位显示)

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

读取图像按固定窗宽窗位显示

相关推荐
马里马里奥-1 天前
从零搭建AI Agent工具链的技术文章大纲
开发语言·qt
Quz1 天前
QML 与 JavaScript 交互方式:内联函数、外部文件、信号槽与工作线程
javascript·qt
Quz1 天前
QML ToolTip 组件:延迟提示与自定义外观
qt
wWYy.1 天前
RPC详解
qt·rpc·php
Fu_Lin_1 天前
Qt嵌入式从零基础到精通(01):Qt嵌入式开发全景图:从桌面Qt到ARM Linux
linux·arm开发·qt·qt5·嵌入式linux
海清河晏1111 天前
Qt实战:从零构建美化登录界面
开发语言·c++·qt
Fu_Lin_1 天前
《Qt嵌入式从零基础到精通》前言与阅读指南
开发语言·qt
Eason_CM1 天前
QT学习教程与实战三:控件与QT Designer
c++·windows·qt·visual studio
mabing9932 天前
Qt 生成条纹图
开发语言·qt