【C++风云录】解析医学影像处理工具:深入探讨ITK、OpenIGTLink、VTK、DCMTK、GDCM和NIfTI的应用和技术

医学影像处理工具:探索ITK、OpenIGTLink、VTK、DCMTK、GDCM和NIfTI

前言

医学影像处理在现代医学中起着至关重要的作用。随着医学技术的不断进步,出现了许多强大的开源工具和库,用于帮助医学影像处理专业人员处理、分析和可视化复杂的医学影像数据。本文将介绍几个常用的医学影像处理工具,包括ITK、OpenIGTLink、VTK、DCMTK、GDCM和NIfTI,探索它们的功能和应用。

欢迎订阅专栏:C++风云录

1. ITK (Insight Segmentation and Registration Toolkit)

1.1 概述

ITK(Insight Segmentation and Registration Toolkit)是一个开源的图像分割和配准工具包,广泛用于医学图像分析和计算机辅助诊断领域。ITK提供了一系列强大的算法和数据结构,用于从医学图像中提取特征、分割图像、配准不同图像之间的空间位置等。

1.2 图像分割

图像分割是指将医学图像中的结构或目标从背景中分离出来的过程。ITK提供了多种图像分割算法,如阈值分割、区域生长、水平集、活动轮廓模型等。以下是一个使用ITK进行阈值分割的示例代码:

#include "itkImage.h"
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkThresholdImageFilter.h"

typedef itk::Image<unsigned char, 2> ImageType;

int main(int argc, char* argv[])
{
    // 读取输入图像
    typedef itk::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName("input.png");
    reader->Update();

    // 阈值分割
    typedef itk::ThresholdImageFilter<ImageType> ThresholdFilterType;
    ThresholdFilterType::Pointer thresholdFilter = ThresholdFilterType::New();
    thresholdFilter->SetInput(reader->GetOutput());
    thresholdFilter->SetOutsideValue(0);
    thresholdFilter->ThresholdAbove(128);
    thresholdFilter->Update();

    // 写入输出图像
    typedef itk::ImageFileWriter<ImageType> WriterType;
    WriterType::Pointer writer = WriterType::New();
    writer->SetFileName("output.png");
    writer->SetInput(thresholdFilter->GetOutput());
    writer->Update();

    return 0;
}
1.3 图像配准

图像配准是指将不同图像之间的空间位置进行对齐的过程,主要用于医学图像的重建、比较和分析。ITK提供了多种图像配准算法,如刚性配准、非刚性配准、多模态配准等。以下是一个使用ITK进行刚性配准的示例代码:

#include "itkImage.h"
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkCenteredTransformInitializer.h"
#include "itkVersorRigid3DTransform.h"
#include "itkResampleImageFilter.h"

typedef itk::Image<float, 3> ImageType;
typedef itk::VersorRigid3DTransform<double> TransformType;

int main(int argc, char* argv[])
{
    // 读取固定图像和移动图像
    typedef itk::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer fixedReader = ReaderType::New();
    fixedReader->SetFileName("fixed.nii");
    fixedReader->Update();

    ReaderType::Pointer movingReader = ReaderType::New();
    movingReader->SetFileName("moving.nii");
    movingReader->Update();

    // 创建配准变换
    TransformType::Pointer transform = TransformType::New();
  
    typedef itk::CenteredTransformInitializer<TransformType, ImageType, ImageType> InitializerType;
    InitializerType::Pointer initializer = InitializerType::New();
    initializer->SetFixedImage(fixedReader->GetOutput());
    initializer->SetMovingImage(movingReader->GetOutput());
    initializer->SetTransform(transform);
    initializer->MomentsOn();
    initializer->InitializeTransform();
  
    // 应用变换
    typedef itk::ResampleImageFilter<ImageType, ImageType> ResampleFilterType;
    ResampleFilterType::Pointer resampleFilter = ResampleFilterType::New();
    resampleFilter->SetInput(movingReader->GetOutput());
    resampleFilter->SetSize(fixedReader->GetOutput()->GetLargestPossibleRegion().GetSize());
    resampleFilter->SetOutputSpacing(fixedReader->GetOutput()->GetSpacing());
    resampleFilter->SetOutputOrigin(fixedReader->GetOutput()->GetOrigin());
    resampleFilter->SetOutputDirection(fixedReader->GetOutput()->GetDirection());
    resampleFilter->SetTransform(transform);
    resampleFilter->Update();

    // 写入输出图像
    typedef itk::ImageFileWriter<ImageType> WriterType;
    WriterType::Pointer writer = WriterType::New();
    writer->SetFileName("registered.nii");
    writer->SetInput(resampleFilter->GetOutput());
    writer->Update();

    return 0;
}
1.4 特征提取和分析

特征提取和分析是指从医学图像中提取有用的信息,如边缘、纹理和形状等,并进行定量分析和比较。ITK提供了多种特征提取和分析的算法,如边缘检测、纹理分析、形状分析等。以下是一个使用ITK进行边缘检测的示例代码:

#include "itkImage.h"
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkCannyEdgeDetectionImageFilter.h"

typedef itk::Image<unsigned char, 2> ImageType;

int main(int argc, char* argv[])
{
    // 读取输入图像
    typedef itk::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName("input.png");
    reader->Update();

    // 边缘检测
    typedef itk::CannyEdgeDetectionImageFilter<ImageType, ImageType> EdgeDetectorType;
    EdgeDetectorType::Pointer edgeDetector = EdgeDetectorType::New();
    edgeDetector->SetInput(reader->GetOutput());
    edgeDetector->SetVariance(2.0);
    edgeDetector->SetLowerThreshold(0.0);
    edgeDetector->SetUpperThreshold(255.0);
    edgeDetector->Update();

    // 写入输出图像
    typedef itk::ImageFileWriter<ImageType> WriterType;
    WriterType::Pointer writer = WriterType::New();
    writer->SetFileName("output.png");
    writer->SetInput(edgeDetector->GetOutput());
    writer->Update();

    return 0;
}
1.5 三维可视化

三维可视化是指将医学图像以三维形式进行展示和呈现的过程,用于更直观地理解和观察图像中的结构和信息。ITK结合了VTK(Visualization Toolkit)来进行三维可视化。以下是一个使用ITK与VTK进行三维可视化的示例代码:

#include "itkImage.h"
#include "itkImageFileReader.h"

#include "itkRescaleIntensityImageFilter.h"

#include "vtkRenderWindowInteractor.h"

#include "itkImageToVTKImageFilter.h"
#include "vtkSmartPointer.h"
#include "vtkImageActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkInteractorStyleTrackballCamera.h"

typedef itk::Image<unsigned char, 3> ImageType;

int main(int argc, char* argv[])
{
    // 读取输入图像
    typedef itk::ImageFileReader<ImageType> ReaderType;
    ReaderType::Pointer reader = ReaderType::New();
    reader->SetFileName("input.nii");
    reader->Update();

    // 图像强度调整
    typedef itk::RescaleIntensityImageFilter<ImageType, ImageType> RescaleFilterType;
    RescaleFilterType::Pointer rescaleFilter = RescaleFilterType::New();
    rescaleFilter->SetInput(reader->GetOutput());
    rescaleFilter->SetOutputMinimum(0);
    rescaleFilter->SetOutputMaximum(255);
    rescaleFilter->Update();

    // 将ITK图像转换为VTK图像
    typedef itk::ImageToVTKImageFilter<ImageType> ImageToVTKFilterType;
    ImageToVTKFilterType::Pointer imageToVTKFilter = ImageToVTKFilterType::New();
    imageToVTKFilter->SetInput(rescaleFilter->GetOutput());
    imageToVTKFilter->Update();

    // 创建VTK渲染器和演员
    vtkSmartPointer<vtkImageActor> imageActor = vtkSmartPointer<vtkImageActor>::New();
    imageActor->SetInputData(imageToVTKFilter->GetOutput());

    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(imageActor);
    renderer->ResetCamera();

    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);

    // 创建交互器
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);
    vtkSmartPointer<vtkInteractorStyleTrackballCamera> style = vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New();
    interactor->SetInteractorStyle(style);

    // 开始显示
    interactor->Initialize();
    interactor->Start();

    return 0;
}

2. OpenIGTLink

2.1 概述

OpenIGTLink 是一个用于实时追踪和导航的开源网络通信协议和工具包。它提供了一种标准化的方式来传输和交换医学图像、跟踪数据和手术设备信息。OpenIGTLink 不仅可以在同一台计算机上实现实时通信,还可以通过网络在分布式系统中进行通信,使跨地域、跨设备的实时追踪和导航成为可能。

2.2 数据传输

OpenIGTLink 使用 TCP/IP 协议在网络上传输数据,以确保可靠性和实时性。它支持高带宽的数据传输,包括高分辨率的医学图像和实时的跟踪数据。OpenIGTLink 还通过压缩和数据流管理来优化网络传输性能。

2.3 设备互操作性

OpenIGTLink 提供了一种标准化的数据格式和接口,以促进不同供应商的手术设备和追踪系统之间的互操作性。它定义了一套常见的消息类型用于传输医学图像、跟踪数据、手术设备参数等信息,并提供了基于事件的机制来实现实时的设备控制和数据交换。

2.4 通信协议

OpenIGTLink 的通信协议基于消息传递模式。发送方将消息打包成 OpenIGTLink 格式,然后通过网络发送给接收方,接收方解析消息并根据内容进行相应处理。消息可以包含多种类型的数据,如图像、标量、矩阵、变换等,以满足不同应用的需求。

2.5 实时追踪和导航

OpenIGTLink 在实时追踪和导航领域具有广泛的应用。它可以用于手术导航系统,实时将医学图像与手术场景对准,辅助医生进行精确的手术操作。同时,OpenIGTLink 还可以用于追踪系统,实时获取和传输跟踪数据,例如手术工具的位置和姿态,用于实时监控和指导手术过程。

下面是一个使用 OpenIGTLink 在发送方发送图像数据,接收方接收并显示图像的示例代码:

发送方代码:

#include "igtlOSUtil.h"
#include "igtlMessageHeader.h"
#include "igtlImageMessage.h"

int main(int argc, char *argv[])
{
  // 创建一个消息
  igtl::ImageMessage::Pointer imageMsg = igtl::ImageMessage::New();

  // 设置消息属性
  imageMsg->SetDeviceName("ImageSender");
  imageMsg->SetDimensions(256, 256, 1);
  imageMsg->SetSpacing(1.0, 1.0, 1.0);
  imageMsg->SetScalarType(igtl::ImageMessage::TYPE_UINT8);
  imageMsg->SetEndian(igtl::ImageMessage::ENDIAN_LITTLE);
  imageMsg->SetOrigin(0.0, 0.0, 0.0);
  imageMsg->SetHeaderVersion(IGTL_HEADER_VERSION_2);

  // 创建图像数据
  int imageSize = imageMsg->GetImageSize();
  unsigned char* image = new unsigned char[imageSize];
  // 将图像数据填充为示例数据
  for (int i = 0; i < imageSize; i++)
  {
    image[i] = i % 256;
  }
  imageMsg->SetScalarPointer(image);
  imageMsg->SetTimeStamp(igtl::TimeStamp::GetSystemTime());

  // 初始化 OpenIGTLink
  igtl::Socket::Pointer socket = igtl::Socket::New();
  int r = socket->CreateServer(argv[1]);
  if (r < 0)
  {
    std::cerr << "Failed to create server socket." << std::endl;
    return -1;
  }
  socket->Listen();

  while (true)
  {
    // 等待连接
    igtl::Socket::Pointer clientSocket;
    clientSocket = socket->WaitForConnection(1000);
    if (clientSocket.IsNull())
    {
      continue;
    }

    // 发送消息
    clientSocket->Send(imageMsg->GetPackPointer(), imageMsg->GetPackSize());

    clientSocket->Close();
  }

  // 释放内存
  delete[] image;

  return 0;
}

接收方代码:

#include "igtlOSUtil.h"
#include "igtlMessageHeader.h"
#include "igtlImageMessage.h"

int main(int argc, char *argv[])
{
  // 初始化 OpenIGTLink
  igtl::Socket::Pointer socket = igtl::Socket::New();
  int r = socket->CreateClient(argv[1], atoi(argv[2]));
  if (r < 0)
  {
    std::cerr << "Failed to create client socket." << std::endl;
    return -1;
  }

  while (true)
  {
    // 等待消息
    igtl::MessageHeader::Pointer headerMsg = igtl::MessageHeader::New();
    headerMsg->InitPack();

    int r = socket->Receive(headerMsg->GetPackPointer(), headerMsg->GetPackSize());
    if (r == 0)
    {
      continue;
    }
    if (r != headerMsg->GetPackSize())
    {
      std::cerr << "Failed to receive message header." << std::endl;
      break;
    }
    headerMsg->Unpack();

    // 判断消息类型
    if (strcmp(headerMsg->GetDeviceType(), "IMAGE") == 0)
    {
      // 接收图像数据
      igtl::ImageMessage::Pointer imageMsg = igtl::ImageMessage::New();
      imageMsg->SetMessageHeader(headerMsg);
      imageMsg->AllocatePack();

      r = socket->Receive(imageMsg->GetPackBodyPointer(), imageMsg->GetPackBodySize());
      if (r != imageMsg->GetPackBodySize())
      {
        std::cerr << "Failed to receive image data." << std::endl;
        break;
      }
      imageMsg->Unpack();

      // 显示图像
      int width = imageMsg->GetWidth();
      int height = imageMsg->GetHeight();
      unsigned char* image = imageMsg->GetScalarPointer();
      // 在这里进行图像显示的操作,这里只是简单打印图像尺寸信息
      std::cout << "Received Image: " << width << " x " << height << std::endl;
    }
  }

  return 0;
}

3. VTK (Visualization Toolkit)

3.1 概述

VTK(Visualization Toolkit)是一个用于科学可视化和计算机图形学的开源软件库。它提供了一系列强大的算法和工具,用于可视化和分析不同类型的数据,包括医学图像、仿真数据、地理空间数据等。VTK 不仅支持二维和三维数据的可视化,还提供了高级的可视化技术,如体渲染、表面重建和动画效果等。

3.2 可视化数据模型

VTK 使用一种数据模型来表示和操作不同类型的数据。其核心数据结构是 vtkDataSet,可以包括点、线、面和体等元素,以及与之关联的属性信息。VTK 还支持更高级的数据模型,如 vtkImageData 用于表示规则网格数据、vtkPolyData 用于表示表面数据、vtkUnstructuredGrid 用于表示非结构化网格数据等。

3.3 可视化算法和技术

VTK 提供了丰富的可视化算法和技术,用于处理和呈现不同类型的数据。例如,VTK 提供了各种滤波器用于数据预处理、插值和重采样;提供了多种可视化技术用于绘制和渲染数据,如点、线、面、矢量和张量的绘制;提供了各种位图和矢量图的输出方式以及交互式可视化工具等。

以下是一个使用 VTK 进行简单的数据可视化的示例代码:

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2)
VTK_MODULE_INIT(vtkInteractionStyle)

#include <vtkSmartPointer.h>
#include <vtkPolyData.h>
#include <vtkPoints.h>
#include <vtkLine.h>
#include <vtkCellArray.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>

int main()
{
    // 创建点云数据
    vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
    points->InsertNextPoint(0.0, 0.0, 0.0);
    points->InsertNextPoint(1.0, 0.0, 0.0);
    points->InsertNextPoint(0.0, 1.0, 0.0);

    // 创建线数据
    vtkSmartPointer<vtkLine> line = vtkSmartPointer<vtkLine>::New();
    line->GetPointIds()->SetId(0, 0);
    line->GetPointIds()->SetId(1, 1);

    // 创建单元格数据
    vtkSmartPointer<vtkCellArray> cells = vtkSmartPointer<vtkCellArray>::New();
    cells->InsertNextCell(line);

    // 创建多边形数据
    vtkSmartPointer<vtkPolyData> polyData = vtkSmartPointer<vtkPolyData>::New();
    polyData->SetPoints(points);
    polyData->SetLines(cells);

    // 创建数据映射器和演员
    vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
    mapper->SetInputData(polyData);

    vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
    actor->SetMapper(mapper);

    // 创建渲染器和窗口
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(actor);

    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);

    // 创建交互器
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);

    // 开始显示
    interactor->Initialize();
    interactor->Start();

    return 0;
}
3.4 体渲染和体绘制

VTK 提供了用于体渲染和体绘制的算法和技术,用于可视化包括医学图像在内的三维体数据。常用的体渲染方法包括体绘制、光线投射和体分割等。以下是一个使用 VTK 进行简单体渲染的示例代码:

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2)
VTK_MODULE_INIT(vtkInteractionStyle)
VTK_MODULE_INIT(vtkVolumeRendering)

#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkVolume.h>
#include <vtkVolumeMapper.h>
#include <vtkFixedPointVolumeRayCastMapper.h>
#include <vtkVolumeProperty.h>
#include <vtkColorTransferFunction.h>
#include <vtkPiecewiseFunction.h>

int main()
{
    // 创建体数据
    vtkSmartPointer<vtkImageData> volumeData = vtkSmartPointer<vtkImageData>::New();
    // 读入体数据,省略...

    // 创建体渲染器和属性
    vtkSmartPointer<vtkVolumeMapper> volumeMapper = vtkSmartPointer<vtkFixedPointVolumeRayCastMapper>::New();
    volumeMapper->SetInputData(volumeData);

    vtkSmartPointer<vtkVolume> volume = vtkSmartPointer<vtkVolume>::New();
    volume->SetMapper(volumeMapper);
  
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddVolume(volume);

    // 设置体颜色和透明度
    vtkSmartPointer<vtkColorTransferFunction> colorTransfer = vtkSmartPointer<vtkColorTransferFunction>::New();
    colorTransfer->AddRGBPoint(0, 0, 0, 0); // 第一个参数为体标记值,后面三个参数为 RGB 颜色值
    colorTransfer->AddRGBPoint(255, 1, 1, 1); // 为最大标记值
    // 设置透明度
    vtkSmartPointer<vtkPiecewiseFunction> opacityTransfer = vtkSmartPointer<vtkPiecewiseFunction>::New();
    opacityTransfer->AddPoint(0, 0);
    opacityTransfer->AddPoint(255, 1);
  
    vtkSmartPointer<vtkVolumeProperty> volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New();
    volumeProperty->SetColor(colorTransfer);
    volumeProperty->SetScalarOpacity(opacityTransfer);
    volumeProperty->ShadeOn();

    volume->SetProperty(volumeProperty);

    // 创建窗口和交互器
    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);

    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);

    // 开始显示
    renderWindow->Render();
    interactor->Start();

    return 0;
}
3.5 表面重建和网格处理

VTK 提供了用于表面重建和网格处理的算法和技术,用于从离散点数据生成平滑的表面模型,或对现有的网格数据进行编辑和处理。常用的表面重建方法包括点云重建、交互式网格建模和曲面重建等。以下是一个使用 VTK 进行点云重建的示例代码:

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2)
VTK_MODULE_INIT(vtkInteractionStyle)
VTK_MODULE_INIT(vtkFiltersModeling)

#include <vtkSmartPointer.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkDelaunay2D.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>

int main()
{
    // 创建点云数据
    vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
    points->InsertNextPoint(0.0, 0.0, 0.0);
    points->InsertNextPoint(1.0, 0.0, 0.0);
    points->InsertNextPoint(0.0, 1.0, 0.0);

    // 创建点云数据的表面
    vtkSmartPointer<vtkPolyData> polyData = vtkSmartPointer<vtkPolyData>::New();
    polyData->SetPoints(points);

    vtkSmartPointer<vtkDelaunay2D> delaunayFilter = vtkSmartPointer<vtkDelaunay2D>::New();
    delaunayFilter->SetInputData(polyData);
    delaunayFilter->Update();

    // 创建数据映射器和演员
    vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
    mapper->SetInputData(delaunayFilter->GetOutput());

    vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
    actor->SetMapper(mapper);

    // 创建渲染器和窗口
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(actor);

    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);

    // 创建交互器
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);

    // 开始显示
    interactor->Initialize();
    interactor->Start();

    return 0;
}

4. DCMTK (DICOM Toolkit)

4.1 概述

DCMTK(DICOM Toolkit)是一个用于处理医学图像和通信的开源软件包。它实现了 DICOM(Digital Imaging and Communications in Medicine)标准,用于医学图像的获取、存储、传输和显示。DCMTK 提供了一组功能丰富的工具和库,用于读取、写入、查询、检索和处理 DICOM 数据。

4.2 DICOM 数据处理

DCMTK 提供了丰富的功能和工具,用于对 DICOM 数据进行处理和操作。它支持 DICOM 文件和数据集的读取和写入,可以轻松地访问和解析 DICOM 数据的各个属性和元素。此外,DCMTK 还提供了灵活的图像处理和转换功能,可以对 DICOM 图像执行各种操作,如缩放、旋转、滤波等。

以下是一个使用 DCMTK 读取和显示 DICOM 图像的示例代码:

#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmimgle/dcmimage.h"

int main(int argc, char* argv[])
{
    // 读取 DICOM 文件
    const char* dicomFile = "image.dcm";
    DicomImage* dcmImage = new DicomImage(dicomFile);
    if (dcmImage->getStatus() != EIS_Normal)
    {
        delete dcmImage;
        return -1;
    }

    // 获取图像数据
    const Uint8* pixelData = (const Uint8*)dcmImage->getOutputData(8 /* 8 bits/pixel */);

    // 获取图像属性
    unsigned int width = dcmImage->getWidth();
    unsigned int height = dcmImage->getHeight();

    // 显示图像
    // 在这里进行图像显示的操作,这里只是简单打印图像尺寸信息
    std::cout << "DICOM Image: " << width << " x " << height << std::endl;

    delete dcmImage;

    return 0;
}
4.3 DICOM 图像读取和写入

DCMTK 提供了丰富的功能和工具,用于对 DICOM 图像进行读取和写入。它支持不同类型的 DICOM 文件和数据集,包括单个图像、多帧图像、序列图像等。通过 DCMTK,您可以轻松地读取和写入 DICOM 图像,并访问和编辑其各个属性和元素。

以下是一个使用 DCMTK 读取并写入 DICOM 图像的示例代码:

#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmimgle/dcmimage.h"

int main(int argc, char* argv[])
{
    // 读取 DICOM 文件
    const char* dicomFile = "input.dcm";
    DicomImage* dcmImage = new DicomImage(dicomFile);
    if (dcmImage->getStatus() != EIS_Normal)
    {
        delete dcmImage;
        return -1;
    }

    // 获取图像数据
    const Uint8* pixelData = (const Uint8*)dcmImage->getOutputData(8);

    // 获取图像属性
    unsigned int width = dcmImage->getWidth();
    unsigned int height = dcmImage->getHeight();

    delete dcmImage;

    // 创建 DICOM 图像
    DicomImage newImage(pixelData, width, height, 8 /* 8 bits/pixel */);

    // 设置图像属性
    newImage.setPixelSpacing(1.0, 1.0);
    newImage.setPhotometricInterpretation("MONOCHROME2");
    // 设置其他图像属性...

    // 保存 DICOM 图像
    const char* outputDicomFile = "output.dcm";
    newImage.write(outputDicomFile, EXS_LittleEndianImplicit);

    return 0;
}
4.4 DICOM 查询和检索

DCMTK 提供了用于查询和检索 DICOM 数据的功能和工具。您可以使用 DCMTK 来构建 DICOM 查询,向远程 DICOM 服务器发送查询请求,并解析和处理查询结果。DCMTK 支持各种查询模式,如基本查询、CBIR(Content-Based Image Retrieval)查询等。

以下是一个使用 DCMTK 发送 DICOM 查询并解析查询结果的示例代码:

#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dctk.h"
#include "dcmtk/dcmsr/dsrdoc.h"

int main(int argc, char* argv[])
{
    // 连接至 DICOM 服务器
    T_ASC_Association* assoc = nullptr;
    const char* remoteHost = "127.0.0.1";
    const int remotePort = 104;

    OFCondition cond = ASC_initializeNetwork(NET_REQUESTOR);
    if (cond.bad())
    {
        return -1;
    }
    cond = ASC_createAssociationParameters(&assocParams, ASC_DEFAULTMAXPDU);
    if (cond.bad())
    {
        return -1;
    }
    cond = ASC_setPresentationAddresses(assocParams, nullptr, remoteHost, remotePort);
    if (cond.bad())
    {
        return -1;
    }

    // 构建查询
    DcmDataset* queryDataset = new DcmDataset;
    // 设置查询条件...

    // 发送查询
    cond = DIMSE_findUser(assoc, DIMSE_FINDSTUDY, queryDataset, nullptr, nullptr, handleFindUserCallback, this);
    if (cond != EC_Normal)
    {
        return -1;
    }

    // 等待查询结果
    cond = DIMSE_getUser(assoc, &msgId, nullptr, nullptr, nullptr, false);
    if (cond != EC_Normal)
    {
        return -1;
    }

    // 解析查询结果
    T_DIMSE_C_FindRSP findRsp;
    cond = DIMSE_parseResponse(msg, status, &findRsp, nullptr, nullptr);
    if (cond != EC_Normal)
    {
        return -1;
    }

    // 处理查询结果
    DcmDataset* studyDataset = findRsp.dicomDataset;
    // 解析和处理查询结果的代码...

    // 关闭连接
    ASC_releaseAssociation(assoc);
    ASC_destroyAssociation(&assoc);

    return 0;
}
4.5 DICOM 网络通信

DCMTK 提供了用于 DICOM 网络通信的功能和工具,您可以使用 DCMTK 在不同的 DICOM 设备之间进行数据传输和交换。DCMTK 支持 DICOM 的网络通信协议和服务类别,如 C-FIND、C-MOVE、C-STORE 等。通过 DCMTK,您可以构建 DICOM 网络通信的客户端和服务器,并与其他 DICOM 设备进行数据交互。

以下是一个使用 DCMTK 构建 DICOM 服务器的示例代码:

#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmnet/dcmnet.h"

int main(int argc, char* argv[])
{
    // 初始化网络和 DICOM
    OFCondition cond = DcmNet::initializeNetwork(NET_ACCEPTOR);
    if (cond.bad())
    {
        return -1;
    }
    cond = DcmNet::initializeNetwork(NET_REQUESTOR);
    if (cond.bad())
    {
        return -1;
    }
    cond = DcmNet::initializeNetwork(NET_BOTH);
    if (cond.bad())
    {
        return -1;
    }

    // 创建 DICOM 服务器
    int listenPort = 11112;
    OFList<OFString> networkTypes;
    networkTypes.push_back("TCP");
    OFList<OFString> transferSyntaxes;
    transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
    DcmAssociationConfiguration config;
    DcmServer server(config);
    cond = server.listenForConnections(listenPort, networkTypes, transferSyntaxes, DU_ACCEPTOR);
    if (cond.bad())
    {
        return -1;
    }

    // 等待连接
    while (server.isListening())
    {
        DcmServerChild* child = server.waitForConnect();
        if (child != nullptr)
        {
            // 处理连接请求
            // 在这里添加处理连接请求的代码...
        }
    }

    // 关闭服务器
    server.close();

    // 清理网络和 DICOM
    DcmNet::shutdownNetwork();

    return 0;
}

5. GDCM (Grassroots DICOM)

5.1 概述

GDCM(Grassroots DICOM)是一个开源的 C++ 库,用于处理 DICOM(Digital Imaging and Communications in Medicine)医学文件。GDCM 提供了解析、转换、编码、解码 DICOM 文件的功能,以及对 DICOM 数据的包装和解包。此外,GDCM 还提供了对 DICOM 图像进行处理和可视化的功能。

5.2 DICOM 文件解析和转换

GDCM 提供了强大的功能,用于解析和转换 DICOM 文件。它可以读取、解析和检索 DICOM 文件的元数据和图像数据。您可以使用 GDCM 将 DICOM 文件转换为其他格式,如 JPEG、PNG、NIfTI 等,以满足不同应用的需求。

以下是一个使用 GDCM 读取和显示 DICOM 图像的示例代码:

#include <string>
#include <iostream>
#include <gdcmReader.h>
#include <gdcmImageReader.h>
#include <gdcmImageWriter.h>
#include <gdcmGlobal.h>
#include <gdcmSystem.h>
#include <gdcmAttribute.h>
#include <gdcmImage.h>
#include <gdcmImageData.h>
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkImageMapper3D.h>
#include <vtkActor.h>
#include <vtkInteractorStyleTrackballCamera.h>

int main()
{
    std::string path = "image.dcm";
    
    gdcm::ImageReader reader;
    reader.SetFileName(path.c_str());
    if (!reader.Read()) {
        std::cerr << "Failed to read the DICOM file: " << path << std::endl;
        return -1;
    }
    
    const gdcm::File &file = reader.GetFile();
    const gdcm::DataSet &dataset = file.GetDataSet();
    
    gdcm::Attribute<0x0028, 0x0010> rows;
    rows.SetFromDataSet(dataset);
    unsigned int num_rows = rows.GetValue();
    
    gdcm::Attribute<0x0028, 0x0011> cols;
    cols.SetFromDataSet(dataset);
    unsigned int num_cols = cols.GetValue();
    
    gdcm::Image image;
    image.SetNumberOfDimensions(2);
    image.SetDimension(0, num_cols);
    image.SetDimension(1, num_rows);
    image.SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB);
    image.SetPixelFormat(gdcm::PixelFormat::UINT8);
    
    gdcm::SmartPointer<gdcm::ImageData> imageData = new gdcm::EncapsulatedPixmap;
    image.SetDataElement(imageData);
    image.SetDataElements(dataset);
    
    vtkSmartPointer<vtkImageData> vtkImage = vtkSmartPointer<vtkImageData>::New();
    vtkImage->SetDimensions(num_cols, num_rows, 1);
    vtkImage->SetSpacing(1.0, 1.0, 1.0);
    vtkImage->SetOrigin(0.0, 0.0, 0.0);
    vtkImage->SetScalarTypeToUnsignedChar();
    vtkImage->SetNumberOfScalarComponents(3);
    vtkImage->AllocateScalars();
    
    memcpy(vtkImage->GetScalarPointer(), imageData->GetBuffer(), num_cols * num_rows * 3);
    
    vtkSmartPointer<vtkImageMapper3D> imageMapper = vtkSmartPointer<vtkImageMapper3D>::New();
    imageMapper->SetInputData(vtkImage);
    
    vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
    actor->SetMapper(imageMapper);
    
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(actor);
    
    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);
    
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);
    interactor->Initialize();
    
    interactor->Start();
    
    return 0;
}
5.3 DICOM 数据包装和解包

GDCM 提供了用于对 DICOM 数据进行包装和解包的功能。它可以将 DICOM 数据包装为各种传输协议和数据格式,如 TCP/IP、UDP、HTTP、XML 等。同时,GDCM 也支持解包已封装的 DICOM 数据,以供后续处理和分析。

以下是一个使用 GDCM 对 DICOM 数据进行包装和解包的示例代码:

#include <iostream>
#include <gdcmGlobal.h>
#include <gdcmReader.h>
#include <gdcmWriter.h>
#include <gdcmImageReader.h>
#include <gdcmImageWriter.h>

int main()
{
    std::string inputFile = "input.dcm";
    std::string outputFile = "output.dcm";
    
    gdcm::Reader reader;
    reader.SetFileName(inputFile.c_str());
    if (!reader.Read()) {
        std::cerr << "Failed to read the input DICOM file: " << inputFile << std::endl;
        return -1;
    }
    
    const gdcm::File &file = reader.GetFile();
    
    gdcm::Writer writer;
    writer.SetFileName(outputFile.c_str());
    writer.SetFile(file);
    if (!writer.Write()) {
        std::cerr << "Failed to write the output DICOM file: " << outputFile << std::endl;
        return -1;
    }
    
    std::cout << "DICOM file successfully processed." << std::endl;
    
    return 0;
}
5.4 DICOM 编码和解码

GDCM 提供了 DICOM 数据的编码和解码功能,包括将不同数据类型转换为 DICOM 格式,以及从 DICOM 数据中提取和解码各种类型的数据。它支持各种编码和解码算法,如 RLE(Run-Length Encoding)、JPEG、JPEG 2000 等。

以下是一个使用 GDCM 进行 DICOM 数据的编码和解码的示例代码:

#include <iostream>
#include <gdcmGlobal.h>
#include <gdcmReader.h>
#include <gdcmWriter.h>
#include <gdcmJPEG2000Codec.h>

int main()
{
    std::string inputFile = "input.dcm";
    std::string outputFile = "output.dcm";
    
    gdcm::Reader reader;
    reader.SetFileName(inputFile.c_str());
    if (!reader.Read()) {
        std::cerr << "Failed to read the input DICOM file: " << inputFile << std::endl;
        return -1;
    }
    
    const gdcm::File &file = reader.GetFile();
    
    gdcm::Writer writer;
    writer.SetFileName(outputFile.c_str());
    writer.SetFile(file);
    
    gdcm::JPEG2000Codec codec;
    codec.SetLossyFlag(true);
    codec.SetLosslessFlag(false);
    codec.SetQuality(0.5);
    codec.SetPixelFormat(gdcm::PixelFormat::UINT8);
    codec.SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB);
    
    writer.SetCodec(&codec);
    
    if (!writer.Write()) {
        std::cerr << "Failed to write the output DICOM file: " << outputFile << std::endl;
        return -1;
    }
    
    std::cout << "DICOM file successfully processed." << std::endl;
    
    return 0;
}
5.5 DICOM 图像处理和可视化

GDCM 提供了用于处理和可视化 DICOM 图像的功能。您可以使用 GDCM 对 DICOM 图像进行像素级别的操作,如缩放、旋转、滤波、窗宽窗位调整等。此外,GDCM 还提供了对 DICOM 图像进行可视化和可视分析的工具和算法。

以下是一个使用 GDCM 对 DICOM 图像进行调整窗宽窗位并可视化的示例代码:

#include <string>
#include <iostream>
#include <gdcmGlobal.h>
#include <gdcmReader.h>
#include <gdcmImageReader.h>
#include <gdcmImageWriter.h>
#include <gdcmImageChangeTransferSyntax.h>
#include <vtkSmartPointer.h>
#include <vtkImageData.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkImageMapper3D.h>
#include <vtkActor.h>
#include <vtkInteractorStyleTrackballCamera.h>

int main()
{
    std::string path = "image.dcm";
    
    gdcm::ImageReader reader;
    reader.SetFileName(path.c_str());
    if (!reader.Read()) {
        std::cerr << "Failed to read the DICOM file: " << path << std::endl;
        return -1;
    }
    
    const gdcm::File &file = reader.GetFile();
    const gdcm::DataSet &dataset = file.GetDataSet();
    
    gdcm::Attribute<0x0028, 0x0010> rows;
    rows.SetFromDataSet(dataset);
    unsigned int num_rows = rows.GetValue();
    
    gdcm::Attribute<0x0028, 0x0011> cols;
    cols.SetFromDataSet(dataset);
    unsigned int num_cols = cols.GetValue();
    
    gdcm::Attribute<0x0028, 0x0100> bitsAllocated;
    bitsAllocated.SetFromDataSet(dataset);
    unsigned int num_bitsAllocated = bitsAllocated.GetValue();
    
    gdcm::Attribute<0x0028, 0x1050> windowCenter;
    windowCenter.SetFromDataSet(dataset);
    double center = windowCenter.GetValue();
    
    gdcm::Attribute<0x0028, 0x1051> windowWidth;
    windowWidth.SetFromDataSet(dataset);
    double width = windowWidth.GetValue();
    
    gdcm::ImageChangeTransferSyntax changeTransferSyntax;
    changeTransferSyntax.SetTransferSyntax(gdcm::TransferSyntax::ExplicitVRLittleEndian);
    changeTransferSyntax.SetInput(reader.GetImage());
    if (!changeTransferSyntax.Change()) {
        return -1;
    }
    
    const double rescale_slope = changeTransferSyntax.GetRescaleSlope();
    const double rescale_intercept = changeTransferSyntax.GetRescaleIntercept();
    
    vtkSmartPointer<vtkImageData> vtkImage = vtkSmartPointer<vtkImageData>::New();
    vtkImage->SetDimensions(num_cols, num_rows, 1);
    vtkImage->SetSpacing(1.0, 1.0, 1.0);
    vtkImage->SetOrigin(0.0, 0.0, 0.0);
    vtkImage->SetScalarTypeToUnsignedChar();
    vtkImage->SetNumberOfScalarComponents(1);
    vtkImage->AllocateScalars();
    
    unsigned char *outputBuffer = static_cast<unsigned char*>(vtkImage->GetScalarPointer());
    const unsigned char *inputBuffer = static_cast<const unsigned char*>(changeTransferSyntax.GetOutput()->GetBuffer());
    const size_t bufferSize = num_rows * num_cols;
    for (size_t i = 0; i < bufferSize; ++i) {
        double value = rescale_slope * inputBuffer[i] + rescale_intercept;
        if (value < center - 0.5 - (width - 1.0) / 2.0) {
            value = 0.0;
        }
        else if (value > center - 0.5 + (width - 1.0) / 2.0) {
            value = width - 1.0;
        }
        else {
            value = std::round((value - (center - 0.5)) / (width - 1.0) * 255.0);
        }
        outputBuffer[i] = static_cast<unsigned char>(value);
    }
    
    vtkSmartPointer<vtkImageMapper3D> imageMapper = vtkSmartPointer<vtkImageMapper3D>::New();
    imageMapper->SetInputData(vtkImage);
    
    vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
    actor->SetMapper(imageMapper);
    
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(actor);
    
    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);
    
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);
    interactor->Initialize();
    
    interactor->Start();
    
    return 0;
}

6. NIfTI (Neuroimaging Informatics Technology Initiative)

6.1 概述

NIfTI(Neuroimaging Informatics Technology Initiative)是一种用于神经影像学的常见数据格式和工具。它是一个开放标准,被广泛用于存储和处理神经影像学数据,如脑部 MRI(Magnetic Resonance Imaging)图像、功能磁共振成像(fMRI)数据等。

6.2 NIfTI 数据格式

NIfTI 数据格式是一种扩展名为 “.nii” 或 “.nii.gz” 的文件格式,基于扩展的 Analyze 数据格式(ANALYZE 7.5)。它支持多种类型的数据集,包括三维体数据、多帧体数据、矢量场和概率图等。NIfTI 格式使用简单的数据结构和元数据,易于与其他神经影像学软件进行交互。

6.3 NIfTI 数据读取和写入

NIfTI 数据格式可以使用各种软件和工具进行读取和写入。您可以使用常见的神经影像学软件包(如 FSL、SPM、AFNI)或编程语言(如 Python、MATLAB)来读取和处理 NIfTI 数据。这些软件提供了相应的 API 或库,用于读取和写入 NIfTI 数据。

以下是一个使用 NIfTI 库读取和写入 NIfTI 数据的示例代码:

#include <nifti1_io.h>

int main()
{
    const char* inputFile = "input.nii";
    const char* outputFile = "output.nii";
    
    nifti_image* inputImage = nifti_image_read(inputFile, true /* 读取数据 */);
    if (inputImage == nullptr) {
        fprintf(stderr, "Failed to read the input NIfTI file: %s\n", inputFile);
        return -1;
    }
    
    // 读取和处理 NIfTI 数据的示例代码...
    
    nifti_image_write(inputImage);
    nifti_image_free(inputImage);
    
    return 0;
}
6.4 NIfTI 数据处理和分析

NIfTI 数据格式可以使用各种工具和算法进行处理和分析。这些工具和算法涵盖了许多神经影像学领域,如脑图像配准、脑分割、功能连接性分析、成像统计分析等。您可以使用现有的软件包或编写自己的代码来实现特定的数据处理和分析任务。

以下是一个使用 NIfTI 库进行脑图像配准的示例代码:

#include <nifti1_io.h>

int main()
{
    const char* fixedFile = "fixed.nii";
    const char* movingFile = "moving.nii";
    const char* outputFile = "registered.nii";
    
    nifti_image* fixedImage = nifti_image_read(fixedFile, true /* 读取数据 */);
    if (fixedImage == nullptr) {
        fprintf(stderr, "Failed to read the fixed NIfTI file: %s\n", fixedFile);
        return -1;
    }
    
    nifti_image* movingImage = nifti_image_read(movingFile, true /* 读取数据 */);
    if (movingImage == nullptr) {
        fprintf(stderr, "Failed to read the moving NIfTI file: %s\n", movingFile);
        return -1;
    }
    
    // 进行脑图像配准的示例代码...
    
    nifti_image_write(outputImage);
    nifti_image_free(fixedImage);
    nifti_image_free(movingImage);
    
    return 0;
}
6.5 NIfTI 可视化和可视分析

NIfTI 数据格式可以使用各种工具进行可视化和可视分析。这些工具提供了直观的界面和交互功能,以帮助研究人员和临床医生更好地理解和分析神经影像学数据。常见的神经影像学软件包和工具可以用于显示、分析和交互式探索 NIfTI 数据。

以下是一个使用 NIfTI 数据进行可视化的示例代码:

#include <string>
#include <iostream>
#include <nifti1_io.h>
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkNIFTIImageReader.h>
#include <vtkImageMapper3D.h>
#include <vtkActor.h>
#include <vtkInteractorStyleTrackballCamera.h>

int main()
{
    std::string path = "brain.nii";
    
    nifti_image* niftiImage = nifti_image_read(path.c_str(), true /* 读取数据 */);
    if (niftiImage == nullptr) {
        std::cerr << "Failed to read the NIfTI file: " << path << std::endl;
        return -1;
    }
    
    vtkSmartPointer<vtkNIFTIImageReader> reader = vtkSmartPointer<vtkNIFTIImageReader>::New();
    reader->SetFileName(path.c_str());
    reader->Update();
    
    vtkSmartPointer<vtkImageMapper3D> imageMapper = vtkSmartPointer<vtkImageMapper3D>::New();
    imageMapper->SetInputConnection(reader->GetOutputPort());
    
    vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
    actor->SetMapper(imageMapper);
    
    vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
    renderer->AddActor(actor);
    
    vtkSmartPointer<vtkRenderWindow> renderWindow = vtkSmartPointer<vtkRenderWindow>::New();
    renderWindow->AddRenderer(renderer);
    
    vtkSmartPointer<vtkRenderWindowInteractor> interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
    interactor->SetRenderWindow(renderWindow);
    interactor->Initialize();
    
    interactor->Start();
    
    return 0;
}

总结

本文全面介绍了医学影像处理工具ITK、OpenIGTLink、VTK、DCMTK、GDCM和NIfTI的功能和应用。这些工具提供了丰富的算法和方法,用于图像分割、配准、特征提取和分析、数据传输、设备互操作性、通信协议、可视化等方面的处理。它们在医学影像领域具有广泛的应用,并为医学影像处理专业人员提供了强大的工具和资源。通过使用这些工具,医学影像处理专业人员能够更准确、更快速地分析和处理医学影像数据,为医学诊断和治疗提供支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

friklogff

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值