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

文章目录

    • 1.实现目标
    • 2.实现过程
      • [2.1 基本原理](#2.1 基本原理)
      • [2.2 C++源码](#2.2 C++源码)
      • [2.3 功能实现](#2.3 功能实现)
        • [2.3.1 材质](#2.3.1 材质)
        • [2.3.2 具体过程](#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

cpp 复制代码
// 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 uid
	FString studyUid;

	// Series uid
	FString seriesUid;

	// Single dicom file instance number
	uint16 instanceNumber = -1;

	// Pixel data
	BYTE* pixelData;

	// Data length
	unsigned long length = 0;

	// Data type
	EDicomDataType dataType;

	// Dicom width
	uint16 width = 0;

	// Dicom height
	uint16 height = 0;
};

UCLASS()
class DICOMVIS_API USeriesData : public UObject
{
	GENERATED_BODY()

public:
#pragma region DicomParam
	// Dicom file study uid
	FString studyUid;

	// Series uid
	FString seriesUid;

	// Dicom data width
	uint16 width = 0;

	// Dicom data heigth
	uint16 height = 0;

	// This dicom data count
	int count = 0;

	// Data type
	EDicomDataType dataType;

	// Window center of this series all dicom
	float windowCenter = 0;

	// Window width of this series all dicom
	float windowWidth = 1;

	// Data range 16 bit is 65535, 8 bit is 255
	float dataRange = 1;

	// Slope 
	float slope = 1;

	// Intercept 
	float intercept = 0; 
#pragma endregion

private:
	// Dicom data map
	TMap<int, TSharedPtr<FDicomData>> dicomDataMap;

public:
	// Constructor
	USeriesData();

	FCriticalSection dicomDataMapLock;

	// Add dicom data
	void AddDicomData(TSharedPtr<FDicomData> dicomData);

	// Get all dicom Data
	TMap<int, TSharedPtr<FDicomData>> GetAllDicomData();

	// Clear
	void ClearDicomData();
};



UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DICOMVIS_API UVolumeDataLoader : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UVolumeDataLoader();

	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 component
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	UStaticMeshComponent* pPlaneMesh = nullptr;

	// Material
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	UMaterial* pMaterial = nullptr;

	// The dicom pixel data uVolumeTexture
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	UVolumeTexture* pVolumeTexture = nullptr;

	// Study uid
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	FString StudyUid;

	// Series uid
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	FString SeriesUid;

	// Current page number.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	int CurrentPageNum = 0;

	// Window Center of dicom
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	float WindowCenter;

	// Window width of dicom
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dicom")
	float WindowWidth;

	// The range of dicom data, range = max - min
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	float Range = 1;

	// Slope value of dicom
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	float Slope = 0;

	// Intercept value of dicom
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	float Intercept = 0;

	// Dicom image width
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	int Width = 0;

	// Dicom image height
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	int Height = 0;

	// Total slice number.
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	int SliceNum = 0;

	// Dicom image depth
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Dicom")
	int Depth = 0;

	// Load dicom data, parse pixel data to volumeTexture and visualize
	UFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")
	void LoadVolumeDicom();

	// Begin dicom auto page
	UFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")
	void BeginAutoPage();

	// End dicom auto page
	UFUNCTION(CallInEditor, BlueprintCallable, Category = "Dicom")
	void EndAutoPage();
private:
	// Dicom series data
	USeriesData* pSeriesData = nullptr;

	// Material instance dynamic
	UMaterialInstanceDynamic* pMaterialInsDy = nullptr;

	// Flag of is auto page.
	bool isAutoPage = false;

	// Parse all dicom files to a series data
	void ParseDicomFilesToSeriesData(TArray<FString>& dicomFilePaths, FString dirPath, USeriesData* pSeriesDicomData);

	// Update Volume texture
	UVolumeTexture* UpdateVolumeTexture(USeriesData* pSeriesDicomData, UVolumeTexture* pTargetVolumeTexture);

	// Set current page number
	void SetCurrentPageNumber(uint16 pageNumber, USeriesData* pSeriesDicomData);

	// Create or update material instance dynamic and it's parameters.
	void UpdateMaterialInstanceDynamic();

	// Check is 16 bit
	bool Is16Bit(USeriesData*& pSeriesDicomData);

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

	// Get all dicom files in directory.
	TArray<FString> GetFilesInFolder(FString directory, FString extension);

	// Get dicom pixel data type
	EDicomDataType GetDicomPixelDataType(DcmDataset*& pDcmDataset);

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;	
};

(2)VolumeDataLoader.cpp

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 layout
	pPlaneMesh = 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 process
	AsyncTask(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 property
		this->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 empty
	if (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 data
	FString 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 file
		if (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 feature
		EPixelFormat 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 bit
		pTargetVolumeTexture->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 data
	uint8* 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 thread
	AsyncTask(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 valid
	if (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 parameter
	pMaterialInsDy->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 page
	if (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测试数据下载:传送门
相关推荐
小梦白7 小时前
RPG增容3:尝试使用MVC结构搭建玩家升级UI(一)
游戏·ui·ue5·mvc
AgilityBaby14 小时前
解决「CPU Virtualization Technology 未开启或被占用」弹窗问题
ue5·游戏引擎·无畏契约·cpu 虚拟化技术
那就摆吧2 天前
U-Net vs. 传统CNN:为什么医学图像分割需要跳过连接?
人工智能·神经网络·cnn·u-net·医学图像
幻雨様2 天前
UE5多人MOBA+GAS 番外篇:同时造成多种类型伤害
ue5
幻雨様3 天前
UE5多人MOBA+GAS 番外篇:同时造成多种类型伤害,以各种属性值的百分比来应用伤害(版本二)
java·前端·ue5
AA陈超3 天前
虚幻引擎5 GAS开发俯视角RPG游戏 #06-11:游戏后效果执行
c++·游戏·ue5·游戏引擎·虚幻
漫步企鹅4 天前
【worklist】worklist的hl7、dicom是什么关系
hl7·dicom·worklist
幻雨様4 天前
UE5多人MOBA+GAS 番外篇:将冷却缩减属性应用到技能冷却中
ue5
不老刘4 天前
如何将DICOM文件制作成在线云胶片
dicom·云影像·云胶片