当前位置: 首页 > news >正文

【UE5医学影像可视化】读取本地Dicom生成VolumeTexture,实现2D显示和自动翻页

文章目录

    • 1.实现目标
    • 2.实现过程
      • 2.1 基本原理
      • 2.2 C++源码
      • 2.3 功能实现
        • 2.3.1 材质
        • 2.3.2 具体过程
    • 3.参考资料

1.实现目标

上篇文章记录了在UE中加载单张Dicom数据生成2D纹理并显示,本篇文章加载本地文件夹内一个序列的所有Dicom数据,在UE中生成VolumeTexture,并实现自动翻页的功能

显示一个序列内的Dicom数据,可以每次单独加载解析单个Dicom数据,或者生成2D纹理数组都可以实现本文的功能,但为了后续VolumeRendering的方便,所以这里直接使用VolumeTexture才实现功能。

在UE中实现的功能GIF动图效果如下:

在这里插入图片描述

2.实现过程

包括本地文件夹内多个dicom数据的读取,在UE中生成VolumeTexture,按照dicom标准转换展示,和自动翻页功能

2.1 基本原理

(1)多个Dicom文件读取
本文默认当前文件夹内的所有Dicom文件都属于同一个序列,对于属于不同序列,或者需要拆体的数据,本文暂不考虑。每张Dicom数据的顺序按照InstanceNumber Tag的值为依据

①获取本地特定文件夹下的所有.dcm后缀的文件

在这里插入图片描述

②基于dcmtk库解析dicom数据,并初始化VolumeDataLoader组件(本文新建的)中的SeriesDataDicomData数据结构,使用dcmtk三方库解析具体的解析流程与上篇文章相同。

在这里插入图片描述

(2)VolumeTexture生成

①创建VolumeTexture,本文这里只考虑无符号的16位格式,其余格式类型类似,这里暂不考虑

在这里插入图片描述

②遍历DicomData的pixelData数据,并拷贝到VolumeTextureBulkData中。需要注意的是UpdateResource需要在GameThread主线程中更新

在这里插入图片描述

(3)按Dicom标准展示
包括Modality转化和VOI转换等,将Dicom数据中的PixelData进行转换得到P-Values,以实现正确的显示效果。

与上篇的内容一致,包括基于斜率截距反算窗宽窗位,在Shader中进行处理和Gamma矫正等,这里不过多赘述。

(4)自动翻页

在Tick中每帧计算当前页,设置动态材质实例中的参数即可。

在这里插入图片描述

2.2 C++源码

VolumeDataLoader组件的完整C++代码如下所示:

(1)VolumeDataLoader.h

// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/VolumeTexture.h"
#include <memory>// DCMTK uses their own verify and check macros.
// Also, they include some effed up windows headers which for example include min and max macros for that
// extra bit of _screw you_
#pragma push_macro("verify")
#pragma push_macro("check")
#undef verify
#undef check
#include "dcmtk/dcmdata/dcdatset.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmdata/dcfilefo.h"
#include "dcmtk/dcmdata/dcpixel.h"
#include "dcmtk/dcmimgle/dcmimage.h"
#pragma pop_macro("verify")
#pragma pop_macro("check")#include "Engine/StaticMeshActor.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "VolumeDataLoader.generated.h"#define UpdateResource UpdateResource/*** Dicom data type enum.*/
UENUM()
enum EDicomDataType
{E_UShort UMETA(DisplayName = "16bit unsigned"),E_Short UMETA(DisplayName = "16bit signed"),E_Byte UMETA(DisplayName = "8bit unsigned"),E_SByte UMETA(DisplayName = "8bit signed")
};USTRUCT()
struct FDicomData
{GENERATED_USTRUCT_BODY()// Dicom file study uidFString studyUid;// Series uidFString seriesUid;// Single dicom file instance numberuint16 instanceNumber = -1;// Pixel dataBYTE* pixelData;// Data lengthunsigned long length = 0;// Data typeEDicomDataType dataType;// Dicom widthuint16 width = 0;// Dicom heightuint16 height = 0;
};UCLASS()
class DICOMVIS_API USeriesData : public UObject
{GENERATED_BODY()public:
#pragma region DicomParam// Dicom file study uidFString studyUid;// Series uidFString seriesUid;// Dicom data widthuint16 width = 0;// Dicom data heigthuint16 height = 0;// This dicom data countint count = 0;// Data typeEDicomDataType dataType;// Window center of this series all dicomfloat windowCenter = 0;// Window width of this series all dicomfloat windowWidth = 1;// Data range 16 bit is 65535, 8 bit is 255float dataRange = 1;// Slope float slope = 1;// Intercept float intercept = 0; 
#pragma endregionprivate:// Dicom data mapTMap<int, TSharedPtr<FDicomData>> dicomDataMap;public:// ConstructorUSeriesData();FCriticalSection dicomDataMapLock;// Add dicom datavoid AddDicomData(TSharedPtr<FDicomData> dicomData);// Get all dicom DataTMap<int, TSharedPtr<FDicomData>> GetAllDicomData();// Clearvoid ClearDicomData();
};UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DICOMVIS_API UVolumeDataLoader : public UActorComponent
{GENERATED_BODY()public:	// Sets default values for this component's propertiesUVolumeDataLoader();virtual void BeginDestroy() override;// The dicom data dir path, that is the reletive path of ue game project directory.UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")FString TargetDirPath = "Data/DicomData";// Plane static mesh componentUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")UStaticMeshComponent* pPlaneMesh = nullptr;// MaterialUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")UMaterial* pMaterial = nullptr;// The dicom pixel data uVolumeTextureUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")UVolumeTexture* pVolumeTexture = nullptr;// Study uidUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")FString StudyUid;// Series uidUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")FString SeriesUid;// Current page number.UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")int CurrentPageNum = 0;// Window Center of dicomUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")float WindowCenter;// Window width of dicomUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")float WindowWidth;// The range of dicom data, range = max - minUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")float Range = 1;// Slope value of dicomUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")float Slope = 0;// Intercept value of dicomUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")float Intercept = 0;// Dicom image widthUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")int Width = 0;// Dicom image heightUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")int Height = 0;// Total slice number.UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")int SliceNum = 0;// Dicom image depthUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")int Depth = 0;// Load dicom data, parse pixel data to volumeTexture and visualizeUFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")void LoadVolumeDicom();// Begin dicom auto pageUFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")void BeginAutoPage();// End dicom auto pageUFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")void EndAutoPage();
private:// Dicom series dataUSeriesData* pSeriesData = nullptr;// Material instance dynamicUMaterialInstanceDynamic* pMaterialInsDy = nullptr;// Flag of is auto page.bool isAutoPage = false;// Parse all dicom files to a series datavoid ParseDicomFilesToSeriesData(TArray<FString>& dicomFilePaths, FString dirPath, USeriesData* pSeriesDicomData);// Update Volume textureUVolumeTexture* UpdateVolumeTexture(USeriesData* pSeriesDicomData, UVolumeTexture* pTargetVolumeTexture);// Set current page numbervoid SetCurrentPageNumber(uint16 pageNumber, USeriesData* pSeriesDicomData);// Create or update material instance dynamic and it's parameters.void UpdateMaterialInstanceDynamic();// Check is 16 bitbool Is16Bit(USeriesData*& pSeriesDicomData);protected:// Called when the game startsvirtual void BeginPlay() override;// Get all dicom files in directory.TArray<FString> GetFilesInFolder(FString directory, FString extension);// Get dicom pixel data typeEDicomDataType GetDicomPixelDataType(DcmDataset*& pDcmDataset);public:	// Called every framevirtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;	
};

(2)VolumeDataLoader.cpp

// Fill out your copyright notice in the Description page of Project Settings.#include "VolumeDataLoader.h"
#include <Kismet/KismetSystemLibrary.h>
#include "HAL/FileManagerGeneric.h"// Sets default values for this component's properties
UVolumeDataLoader::UVolumeDataLoader()
{// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features// off to improve performance if you don't need them.PrimaryComponentTick.bCanEverTick = true;// Create sub plane mesh for target layoutpPlaneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TargetPlaneMesh"));static ConstructorHelpers::FObjectFinder<UStaticMesh> planeAsset(TEXT("/Engine/BasicShapes/Plane.Plane"));if (planeAsset.Succeeded()){pPlaneMesh->SetStaticMesh(planeAsset.Object);}
}void UVolumeDataLoader::BeginDestroy()
{if (pSeriesData != nullptr){pSeriesData->RemoveFromRoot();pSeriesData->ClearDicomData();pSeriesData = nullptr;}if (pVolumeTexture != nullptr){pVolumeTexture->RemoveFromRoot();pVolumeTexture = nullptr;}if (pPlaneMesh){pPlaneMesh->SetMaterial(0, nullptr);}if (pMaterialInsDy){pMaterialInsDy->ConditionalBeginDestroy();pMaterialInsDy = nullptr;}Super::BeginDestroy();
}void UVolumeDataLoader::LoadVolumeDicom()
{FString dicomDir = UKismetSystemLibrary::GetProjectDirectory() + TargetDirPath;if (!FPaths::DirectoryExists(dicomDir)){UE_LOG(LogTemp, Error, TEXT("dicom dir path is not exist, please check!"));return;}// Load dicom data and parse in other thread processAsyncTask(ENamedThreads::AnyThread, [=, this]() {// TArray<FString> dicomFilePaths = GetFilesInFolder(dicomDir, FString("*.dcm"));if (pSeriesData == nullptr){pSeriesData = NewObject<USeriesData>();// Avoid gc auto collect and destroy.pSeriesData->AddToRoot();}else pSeriesData->ClearDicomData();// Assume all dicom file in the this dir with same series uid.// ToDo: Use series uid and other tag to spilt dicom files into different series data.ParseDicomFilesToSeriesData(dicomFilePaths, dicomDir, pSeriesData);// Set propertythis->StudyUid = pSeriesData->studyUid;this->SeriesUid = pSeriesData->seriesUid;this->WindowCenter = pSeriesData->windowCenter;this->WindowWidth = pSeriesData->windowWidth;this->Slope = pSeriesData->slope;this->Intercept = pSeriesData->intercept;this->Range = pSeriesData->dataRange;this->Width = pSeriesData->width;this->Height = pSeriesData->height;this->SliceNum = pSeriesData->count;this->Depth = Is16Bit(pSeriesData) ? 16 : 8;pVolumeTexture = UpdateVolumeTexture(pSeriesData, pVolumeTexture);UpdateMaterialInstanceDynamic();if (pPlaneMesh){pPlaneMesh->SetMaterial(0, pMaterialInsDy);}});
}void UVolumeDataLoader::BeginAutoPage()
{this->isAutoPage = true;UE_LOG(LogTemp, Log, TEXT("Auto page begin!"));
}void UVolumeDataLoader::EndAutoPage()
{this->isAutoPage = false;UE_LOG(LogTemp, Log, TEXT("Auto page end!"));
}void UVolumeDataLoader::ParseDicomFilesToSeriesData(TArray<FString>& dicomFilePaths, FString dirPath, USeriesData* pSeriesDicomData)
{// Check dicomFilePath is emptyif (dicomFilePaths.IsEmpty()){UE_LOG(LogTemp, Warning, TEXT("the dicom file paths array is empty, please check!"));return;}UE_LOG(LogTemp, Log, TEXT("ParseDicomFilesToSeriesData begin!"));DcmFileFormat fileFormat;// Set series data study uid by the first dicom dataFString firstPath = FPaths::Combine(dirPath, dicomFilePaths[0]);if (fileFormat.loadFile(TCHAR_TO_ANSI(*firstPath)).good()){DcmDataset* dataset = fileFormat.getDataset();pSeriesDicomData->dataType = GetDicomPixelDataType(dataset);pSeriesDicomData->dataRange = Is16Bit(pSeriesDicomData) ? 65535 : 255;pSeriesDicomData->count = dicomFilePaths.Num();OFString ofStudyUid;dataset->findAndGetOFString(DCM_StudyInstanceUID, ofStudyUid);pSeriesDicomData->studyUid = ofStudyUid.c_str();OFString ofSeriesUid;dataset->findAndGetOFString(DCM_SeriesInstanceUID, ofSeriesUid);pSeriesDicomData->seriesUid = ofSeriesUid.c_str();Float64 tagValue;dataset->findAndGetFloat64(DCM_WindowWidth, tagValue);pSeriesDicomData->windowWidth = tagValue;dataset->findAndGetFloat64(DCM_WindowCenter, tagValue);pSeriesDicomData->windowCenter = tagValue;dataset->findAndGetFloat64(DCM_RescaleSlope, tagValue);pSeriesDicomData->slope = tagValue;dataset->findAndGetFloat64(DCM_RescaleIntercept, tagValue);pSeriesDicomData->intercept = tagValue;dataset->findAndGetUint16(DCM_Columns, pSeriesDicomData->width);dataset->findAndGetUint16(DCM_Rows, pSeriesDicomData->height);dataset->clear();fileFormat.clear();}for (int i = 0; i < dicomFilePaths.Num(); i++){FString path = dicomFilePaths[i];FString dicomFilePath = FPaths::Combine(dirPath, path);// Use dcmtk lib to parse dicom fileif (fileFormat.loadFile(TCHAR_TO_ANSI(*dicomFilePath)).good()){TSharedPtr<FDicomData> dicomData = MakeShareable(new FDicomData());DcmDataset* dataset = fileFormat.getDataset();dicomData->dataType = GetDicomPixelDataType(dataset);// Instance number tag may US or IS type, current use IS type.if (!dataset->findAndGetUint16(DCM_InstanceNumber, dicomData->instanceNumber).good()){OFString ofInstanceNumber;dataset->findAndGetOFString(DCM_InstanceNumber, ofInstanceNumber);dicomData->instanceNumber = OFStandard::atof(ofInstanceNumber.c_str());}OFString ofStudyUid;dataset->findAndGetOFString(DCM_StudyInstanceUID, ofStudyUid);dicomData->studyUid = ofStudyUid.c_str();OFString ofSeriesUid;dataset->findAndGetOFString(DCM_SeriesInstanceUID, ofSeriesUid);dicomData->seriesUid = ofSeriesUid.c_str();uint8* pixelData;DcmElement* pixelDataElement;dataset->findAndGetElement(DCM_PixelData, pixelDataElement);dicomData->length = pixelDataElement->getLength();pixelDataElement->getUint8Array(pixelData);dicomData->pixelData = new uint8[dicomData->length];memmove(dicomData->pixelData, pixelData, dicomData->length);dataset->findAndGetUint16(DCM_Columns, dicomData->width);dataset->findAndGetUint16(DCM_Rows, dicomData->height);pSeriesDicomData->AddDicomData(dicomData);fileFormat.clear();dataset->clear();UE_LOG(LogTemp, Log, TEXT("ParseDicomFilesToSeriesData, currentNum: %d, totalNum: %d"), i + 1, dicomFilePaths.Num());}}UE_LOG(LogTemp, Log, TEXT("ParseDicomFilesToSeriesData end!"));
}UVolumeTexture* UVolumeDataLoader::UpdateVolumeTexture(USeriesData* pSeriesDicomData, UVolumeTexture* pTargetVolumeTexture)
{UE_LOG(LogTemp, Log, TEXT("Update volume texture begin!"));if (pSeriesDicomData == nullptr){UE_LOG(LogTemp, Warning, TEXT("the input USeriesData is nullptr, please check!"));return pTargetVolumeTexture;}// Check create or update volume texture.if (pTargetVolumeTexture == nullptr){// ToDo: current just support unsigned r16 format, support the other format in featureEPixelFormat pixelFormat = Is16Bit(pSeriesDicomData) ? EPixelFormat::PF_G16 : EPixelFormat::PF_R8;pTargetVolumeTexture = UVolumeTexture::CreateTransient(pSeriesDicomData->width, pSeriesDicomData->height, pSeriesDicomData->count, pixelFormat);pTargetVolumeTexture->AddToRoot();pTargetVolumeTexture->MipGenSettings = TMGS_NoMipmaps;pTargetVolumeTexture->CompressionSettings = TC_Grayscale;// srgb may not effect for 16 bitpTargetVolumeTexture->SRGB = true;pTargetVolumeTexture->NeverStream = true;pTargetVolumeTexture->Filter = TextureFilter::TF_Nearest;pTargetVolumeTexture->AddressMode = TextureAddress::TA_Clamp;UE_LOG(LogTemp, Log, TEXT("Create volume texture successful!"));}// Update volume texture pixel datauint8* pixelData = static_cast<uint8*>(pTargetVolumeTexture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE));auto dicomDataMap = pSeriesDicomData->GetAllDicomData();for (int i = 1; i <= dicomDataMap.Num(); i++){if (!dicomDataMap.Contains(i)){UE_LOG(LogTemp, Warning, TEXT("InstanceNumber of %d is not exist!"), i);return pTargetVolumeTexture;}auto itemDicomData = dicomDataMap[i].Get();uint8* targetData = pixelData + (i - 1) * itemDicomData->length;FMemory::Memcpy(targetData, itemDicomData->pixelData, itemDicomData->length);}pTargetVolumeTexture->GetPlatformData()->Mips[0].BulkData.Unlock();// Update Resource must in game threadAsyncTask(ENamedThreads::GameThread, [=, this]() {pTargetVolumeTexture->UpdateResource();});UE_LOG(LogTemp, Log, TEXT("Update volume texture end!"));return pTargetVolumeTexture;
}void UVolumeDataLoader::SetCurrentPageNumber(uint16 pageNumber, USeriesData* pSeriesDicomData)
{// Check input page number is validif (pageNumber < 0 || pageNumber >= pSeriesDicomData->count){UE_LOG(LogTemp, Error, TEXT("Current input number is not valid!"));return;}float calPagrNumber = (pageNumber + 0.5) / pSeriesDicomData->count;// Set material parameterpMaterialInsDy->SetScalarParameterValue(FName("PageNum"), calPagrNumber);if (pPlaneMesh){pPlaneMesh->SetMaterial(0, pMaterialInsDy);}
}void UVolumeDataLoader::UpdateMaterialInstanceDynamic()
{if (pMaterial == nullptr){UE_LOG(LogTemp, Warning, TEXT("The target volume material is nullptr!"));return;}if (pMaterialInsDy == nullptr){pMaterialInsDy = UMaterialInstanceDynamic::Create(pMaterial, nullptr);}// Update material parameters in game thread.AsyncTask(ENamedThreads::GameThread, [=, this]() {pMaterialInsDy->SetTextureParameterValue(FName("VolumeTex"), pVolumeTexture);// inverset tranform window center and width by slope and intercept;FFloat16 transWL = (this->WindowCenter - this->Intercept) / this->Slope * 1 / 65535.0;FFloat16 transWW = (this->WindowWidth) / this->Slope * 1 / 65535.0;pMaterialInsDy->SetScalarParameterValue(FName("WindowCenter"), transWL);pMaterialInsDy->SetScalarParameterValue(FName("WindowWidth"), transWW);pMaterialInsDy->SetScalarParameterValue(FName("DataRange"), 65535.0);SetCurrentPageNumber(this->CurrentPageNum, pSeriesData);});
}bool UVolumeDataLoader::Is16Bit(USeriesData*& pSeriesDicomData)
{return pSeriesDicomData->dataType == E_UShort || pSeriesDicomData->dataType == E_Short;
}// Called when the game starts
void UVolumeDataLoader::BeginPlay()
{Super::BeginPlay();// ...}TArray<FString> UVolumeDataLoader::GetFilesInFolder(FString directory, FString extension)
{TArray<FString> resFilePaths;resFilePaths.Empty();if (FPaths::DirectoryExists(directory)){FFileManagerGeneric::Get().FindFiles(resFilePaths, *directory, *extension);}return resFilePaths;
}EDicomDataType UVolumeDataLoader::GetDicomPixelDataType(DcmDataset*& pDcmDataset)
{EDicomDataType eDataType = EDicomDataType::E_UShort;Uint16 bitsAllocated;pDcmDataset->findAndGetUint16(DCM_BitsAllocated, bitsAllocated);Uint8 pixelRepresentation = 0;pDcmDataset->findAndGetUint8(DCM_PixelRepresentation, pixelRepresentation);if (bitsAllocated == 8)  eDataType = pixelRepresentation == 1 ? EDicomDataType::E_SByte :  EDicomDataType::E_Byte;else  eDataType = pixelRepresentation == 1 ? EDicomDataType::E_Short : EDicomDataType::E_UShort;return eDataType;
}// Called every frame
void UVolumeDataLoader::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{Super::TickComponent(DeltaTime, TickType, ThisTickFunction);// Update pageif (this->isAutoPage && pSeriesData != nullptr){int pageNum = (this->CurrentPageNum + 1) >= pSeriesData->count ? 0 : (this->CurrentPageNum + 1);this->CurrentPageNum = pageNum;SetCurrentPageNumber(pageNum, pSeriesData);}
}USeriesData::USeriesData()
{dicomDataMap = TMap<int, TSharedPtr<FDicomData>>();
}void USeriesData::AddDicomData(TSharedPtr<FDicomData> dicomData)
{FScopeLock Lock(&dicomDataMapLock);dicomDataMap.Emplace(dicomData->instanceNumber, dicomData);
}TMap<int, TSharedPtr<FDicomData>> USeriesData::GetAllDicomData()
{FScopeLock Lock(&dicomDataMapLock);return dicomDataMap;
}void USeriesData::ClearDicomData()
{FScopeLock Lock(&dicomDataMapLock);for (auto& ele : dicomDataMap){delete ele.Value.Get();ele.Value.Reset();}dicomDataMap.Empty();
}

2.3 功能实现

2.3.1 材质

(1)创建Surface材质,用于最终在StaticMeshComponent(本文这里使用的是UE自带的PlaneMesh)上显示Dicom

其中PageNum参数表示当前从VolumeTexture中采样的Z坐标,范围是[0, 1]
WindowWidthWindowCenter表示当前的窗宽窗位,DataRange参数表示当前数据的范围,如果是16位,则是65535,否则8位255

在这里插入图片描述

(2)VOI兴趣区变换,和上篇文章中的处理方式相同,这里不赘述

在这里插入图片描述

2.3.2 具体过程

(1)在场景中的Actor(可以随便找一个空的Actor)下添加VolumeDataLoader组件

在这里插入图片描述

(2)测试数据,共272张Dicom数据,放在项目工程的Data文件夹下

在这里插入图片描述

(3)配置Dicom数据存放的文件夹的相对路径以及材质

在这里插入图片描述

(4)运行游戏后,点击LoadVolumeDicom按钮,即会加载和解析Dicom数据,并生成VolumeTexture,并默认使用首张Dicom数据进行展示

在这里插入图片描述

显示首张Dicom时的截图:

在这里插入图片描述

(5)自动翻页的开始与暂停

在这里插入图片描述

3.参考资料

  • 【UE5医学影像可视化】读取dicom数据生成2D纹理并显示:传送门
  • Dicom测试数据下载:传送门
http://www.dtcms.com/a/312990.html

相关文章:

  • 关于记录一下“bug”,在做图片上传的时候出现的小问题
  • B3953 [GESP202403 一级] 找因数
  • 大模型智能体(Agent)技术全景:架构演进、协作范式与应用前沿
  • Python Dash 全面讲解
  • 使用 Vuepress + GitHub Pages 搭建项目文档
  • io_getevents系统调用及示例
  • Android 之 图片加载(Fresco/Picasso/Glide)
  • 第四章:OSPF 协议
  • Docker环境离线安卓安装指南
  • Android 之 存储(Assets目录,SharedPreferences,数据库,内部存储等)
  • 音视频学习(五十):音频无损压缩
  • 使用 Docker 部署 Golang 程序
  • 计数组合学7.12( RSK算法的一些推论)
  • 考研复习-计算机组成原理-第二章-数据的表示和运算
  • PHP面向对象编程与数据库操作完全指南-下
  • 深入解析C++函数重载:从原理到实践
  • 2025年测绘程序设计比赛--基于统计滤波的点云去噪(已获国特)
  • MySQL梳理三:查询与优化
  • python新功能match case|:=|typing
  • Hertzbeat如何配置redis?保存在redis的数据是可读数据
  • 【MySQL安全】什么是SQL注入,怎么避免这种攻击:前端防护、后端orm框架、数据库白名单
  • Android设备认证体系深度解析:GMS/CTS/GTS/VTS/STS核心差异与认证逻辑
  • ELECTRICAL靶机复现练习笔记
  • Leetcode:1.两数之和
  • Java 大视界 -- Java 大数据机器学习模型在金融市场情绪分析与投资决策辅助中的应用(379)
  • ubuntu24.04安装selenium、edge、msedgedriver
  • 05.Redis 图形工具RDM
  • 前端开发(HTML,CSS,VUE,JS)从入门到精通!第四天(DOM编程和AJAX异步交互)
  • k8s+isulad 国产化技术栈云原生技术栈搭建1-VPC
  • 使用ACK Serverless容器化部署大语言模型FastChat