文章目录
- 前言
- 一、程序中的类介绍
-
- [1. 解析器类DcmParser](#1. 解析器类DcmParser)
- [2. 数据接口类CBaseImage, CDicomImage](#2. 数据接口类CBaseImage, CDicomImage)
- [3. 图像显示窗口类Displayer](#3. 图像显示窗口类Displayer)
- [4. 绘制参数类DrawParam](#4. 绘制参数类DrawParam)
- [5. 界面对话框类CDcmImageDlg](#5. 界面对话框类CDcmImageDlg)
- 二、代码
-
- [1. DcmParser类](#1. DcmParser类)
- [2. CBaseImage, CDicomImage类](#2. CBaseImage, CDicomImage类)
- [3. Displayer类](#3. Displayer类)
- [4. DrawParam类](#4. DrawParam类)
- [5. CDcmImageDlg类](#5. CDcmImageDlg类)
前言
本章介绍使用dcmtk解析dicom文件的方法。
- DcmFileFormat打开文件
- DcmDataset读取TAG,获取文字信息
- DicomImage读取图像数据,并生成显示用位图数据
程序界面还是使用简单的MFC对话框。界面左边的文件加载区,不再赘述,参考基于dcmtk的dicom工具 第二章 dicom文件tag读取与修改工具
效果如下:

一、程序中的类介绍
tree
└── CDcmImageDlg 对话框界面类
└── CDisplayer 图像显示窗口类,从CWnd派生
├── CDicomImage(CBaseImage) 数据接口类
│ └── DcmParser 解析器类
└── DrawParam 绘制参数类
1. 解析器类DcmParser
- Open函数,调用DcmFileFormat打开dicom文件
- GetTagValue, GetTagStringValue, GetTagIntValue, GetTagFloatValue,四个函数中调用DcmDataset类的findAndGetOFString函数读取Dicom Tag值
- CreateDIB函数,根据DcmFileFormat::getDataset()创建DicomImage对象处理图像数据,可设置窗宽窗位,负像、实现图像缩放、旋转、翻转等功能,最后调用DicomImage::createWindowsDIB生成可用于绘制的位图数据。
- Export函数,实现把dicom文件转换成其他格式的图像,如bmp、jpg、png、tiff、mp4等格式,此函数在后续章节中介绍。
2. 数据接口类CBaseImage, CDicomImage
- 父类CBaseImage定义接口,方便加载多种类型图像,如jpg, png, dicom
- CDicomImage类,从CBaseImage派生,包含一个DcmParser类对象,专门读取dicom文件,ParseFile函数解析文件,患者信息保存到PatientInfo、检查信息保存到StudyInfo、序列信息保存到SeriesInfo、图像信息保存到ImageInfo。CreateDIB函数 生成位图数据
- 后续如果要加载jpg、png,可以从CBaseImage派生一个CColorImage类来读取jpg、png等图像,并从与jpg、png图像关联的文本文件中加载检查信息、检查信息、检查信息、图像信息。本章不涉及此功能。
3. 图像显示窗口类Displayer
从MFC CWnd派生,包含CBaseImage类,调用CDicomImage类读取图像信息,位图数据,最后显示图像、文字。并接受鼠标消息,进行调窗、缩放、旋转等操作。
4. 绘制参数类DrawParam
记录Displayer类当前绘制状态,如图像大小,缩放系数、窗宽窗位、旋转角度等。
5. 界面对话框类CDcmImageDlg
包含Displayer类显示图像、文字。
包含DrawParam类记录当前绘制状态。
文件加载区加载dicom文件,操作按钮区添加各类功能
二、代码
1. DcmParser类
简要描述
重点 DcmParser类中Open函数,CreateDIB函数中已经处理了单帧图和多帧图
只需要在调用CreateDIB时,指定第六个参数frame即可获取某一帧的位图数据。
头文件DcmParser.h:
cpp
#pragma once
class DcmParser
{
public:
DcmParser();
virtual ~DcmParser();
public:
static void RegistryCodecs();
BOOL Open(std::string dcmfile);
BOOL Close();
std::string GetFilePath() { return m_Path; }
BOOL IsValid() { return m_bParserValid; }
BOOL HasImage() { return m_hasImage; }
int GetFrameCount() { return m_nFrameCount; }
CString GetTagValue(unsigned short g, unsigned short e, unsigned long pos=0);
std::string GetTagStringValue(unsigned short g, unsigned short e, unsigned long pos = 0);
int GetTagIntValue(unsigned short g, unsigned short e, unsigned long pos = 0);
float GetTagFloatValue(unsigned short g, unsigned short e, unsigned long pos = 0);
void GetDefaultWindow(double& defWC, double& defWW);
//
/** 根据参数生成位图数据
@param pdib, out,返回位图数据指针,由调用者管理
@param w, h, out,返回图像宽高
@param wc, ww, in, 指定窗宽窗位
@param frame, in, 指定帧数, 索引从0开始
@param bneg, in, 指定是否负像
*/
BOOL CreateDIB(void*& pdib, int& w, int& h, double wc, double ww, int frame=0, bool bneg = false);
bool Export(std::string dst, int format);
int SaveToTiff(DicomImage* di, std::string out, int frame);
BOOL IsRGB();
private:
DcmFileFormat m_dcmFile;
DicomImage* m_pDcmImg;
E_TransferSyntax m_newXfer;
E_TransferSyntax m_orgXfer;
std::string m_Path;
BOOL m_bParserValid;
BOOL m_hasImage;
int m_nFrameStart;
int m_nFrameCount;
int m_nLoadCount;
OFString m_PhotometricOrig;
double m_defWC;
double m_defWW;
DicomImage* createImage(int frame);
};
源文件DcmParser.cpp
cpp
#include "pch.h"
#include "DcmParser.h"
#include "Utilities.h"
#include "tiffio.h"
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#define WIDTHBYTES(bits) ((DWORD)(((bits)+31) & (~31)) / 8)
DcmParser::DcmParser()
: m_bParserValid(FALSE)
, m_hasImage(FALSE)
, m_pDcmImg(nullptr)
, m_defWC(40.0)
, m_defWW(400.0)
, m_nFrameCount(1)
, m_nFrameStart(0)
, m_nLoadCount(1)
{
}
DcmParser::~DcmParser()
{
Close();
}
void DcmParser::RegistryCodecs()
{
DJDecoderRegistration::registerCodecs();
DJLSDecoderRegistration::registerCodecs();
DcmRLEDecoderRegistration::registerCodecs();
DcmRLEEncoderRegistration::registerCodecs();
DJEncoderRegistration::registerCodecs();
DJLSEncoderRegistration::registerCodecs();
}
BOOL DcmParser::Open(std::string dcmfile)
{
if (m_bParserValid)
return TRUE;
dcmAcceptUnexpectedImplicitEncoding.set(OFFalse);
dcmPreferVRFromDataDictionary.set(OFFalse);
OFCondition cond;
cond = m_dcmFile.loadFile(dcmfile.c_str());
if (cond.bad())
return FALSE;
DcmDataset* pDataset = m_dcmFile.getDataset();
Sint32 nFrameCount;
pDataset->findAndGetSint32(DCM_NumberOfFrames, nFrameCount);
if (nFrameCount == 0) m_nFrameCount = 1;
else m_nFrameCount = nFrameCount;
m_orgXfer = m_newXfer = pDataset->getOriginalXfer();
DcmXfer xfer(m_newXfer);
if (xfer.usesEncapsulatedFormat() && m_nFrameCount==1)
{
m_newXfer = EXS_LittleEndianExplicit;
cond = pDataset->chooseRepresentation((E_TransferSyntax)m_newXfer, NULL);
}
//OFString PhotometricOrig;
pDataset->findAndGetOFString(DCM_PhotometricInterpretation, m_PhotometricOrig);
OFCondition wwConf;
wwConf = pDataset->findAndGetFloat64(DCM_WindowCenter, m_defWC);
wwConf = pDataset->findAndGetFloat64(DCM_WindowWidth, m_defWW);
if (wwConf.bad() && m_PhotometricOrig == "RGB")
{
m_defWC = 128;
m_defWW = 256;
wwConf = EC_Normal;
}
m_Path = dcmfile;
m_bParserValid = TRUE;
m_hasImage = TRUE;
if (m_nFrameCount == 1)
{
m_pDcmImg = new DicomImage(pDataset, m_newXfer);
}
else
{
m_nLoadCount = m_nFrameCount > 5 ? 5 : m_nFrameCount;
m_nFrameStart = 0;
unsigned long flag = CIF_UsePartialAccessToPixelData;
m_pDcmImg = new DicomImage(pDataset, m_orgXfer, flag, m_nFrameStart, m_nLoadCount);
}
if (m_pDcmImg->getStatus() != EIS_Normal)
m_hasImage = FALSE;
if (wwConf.bad() && m_nFrameCount > 1) {
DcmItem *pPerFrameItem = NULL;
OFString defWinCenter, defWinWidth;
cond = pDataset->findAndGetSequenceItem(DCM_SharedFunctionalGroupsSequence, pPerFrameItem, 0);
if (pPerFrameItem)
{
DcmItem* pPlanePositionItem = NULL, *pPlaneOrientationItem = NULL, *pVoiLutSeqItem = NULL, *pPixelValueTransformation = NULL,
*pPixelMeasureSeqItem = NULL;
cond = pPerFrameItem->findAndGetSequenceItem(DCM_FrameVOILUTSequence, pVoiLutSeqItem, 0);
if (pVoiLutSeqItem)
{
cond = pVoiLutSeqItem->findAndGetOFString(DCM_WindowCenter, defWinCenter, 0);
cond = pVoiLutSeqItem->findAndGetOFString(DCM_WindowWidth, defWinWidth, 0);
m_defWC = atof(defWinCenter.c_str());
m_defWW = atof(defWinWidth.c_str());
wwConf = EC_Normal;
}
}
}
if (wwConf.bad() && m_pDcmImg->isMonochrome())
{
double min, max;
m_pDcmImg->getMinMaxValues(min, max);
m_defWC = (max - min + 1) / 2.0 + min;
m_defWW = max - min + 1;
wwConf = EC_Normal;
}
return m_bParserValid;
}
BOOL DcmParser::Close()
{
OFCondition cond;
cond = m_dcmFile.clear();
if (m_pDcmImg)
{
delete m_pDcmImg;
m_pDcmImg = nullptr;
}
m_bParserValid = FALSE;
return cond.good() ? TRUE : FALSE;
}
CString DcmParser::GetTagValue(unsigned short g, unsigned short e, unsigned long pos/*=0*/)
{
if (!m_bParserValid)
return _T("");
OFString val;
DcmDataset* pDataset = m_dcmFile.getDataset();
DcmMetaInfo* pMetaInfo = m_dcmFile.getMetaInfo();
if (g == 0x0002)
pMetaInfo->findAndGetOFString(DcmTagKey(g, e), val, pos);
else
pDataset->findAndGetOFString(DcmTagKey(g, e), val, pos);
return val.c_str();
}
std::string DcmParser::GetTagStringValue(unsigned short g, unsigned short e, unsigned long pos /*= 0*/)
{
if (!m_bParserValid)
return "";
OFString val;
DcmDataset* pDataset = m_dcmFile.getDataset();
DcmMetaInfo* pMetaInfo = m_dcmFile.getMetaInfo();
if (g == 0x0002)
pMetaInfo->findAndGetOFString(DcmTagKey(g, e), val, pos);
else
pDataset->findAndGetOFString(DcmTagKey(g, e), val, pos);
return val.c_str();
}
int DcmParser::GetTagIntValue(unsigned short g, unsigned short e, unsigned long pos /*= 0*/)
{
if (!m_bParserValid)
return 0;
OFString val;
DcmDataset* pDataset = m_dcmFile.getDataset();
DcmMetaInfo* pMetaInfo = m_dcmFile.getMetaInfo();
if (g == 0x0002)
pMetaInfo->findAndGetOFString(DcmTagKey(g, e), val, pos);
else
pDataset->findAndGetOFString(DcmTagKey(g, e), val, pos);
if (!val.empty())
return atoi(val.c_str());
return 0;
}
float DcmParser::GetTagFloatValue(unsigned short g, unsigned short e, unsigned long pos /*= 0*/)
{
if (!m_bParserValid)
return 0.0;
OFString val;
DcmDataset* pDataset = m_dcmFile.getDataset();
DcmMetaInfo* pMetaInfo = m_dcmFile.getMetaInfo();
if (g == 0x0002)
pMetaInfo->findAndGetOFString(DcmTagKey(g, e), val, pos);
else
pDataset->findAndGetOFString(DcmTagKey(g, e), val, pos);
if (!val.empty())
return atof(val.c_str());
return 0.0;
}
void DcmParser::GetDefaultWindow(double& defWC, double& defWW)
{
defWC = m_defWC;
defWW = m_defWW;
}
BOOL DcmParser::CreateDIB(void*& pdib, int& w, int& h, double wc, double ww, int frame/* = 0*/, bool bneg/* = false*/)
{
DcmDataset* pDataset = m_dcmFile.getDataset();
int fend = m_nFrameStart + m_nLoadCount - 1;
if (!(frame >= m_nFrameStart && frame <= fend))
{
m_nFrameStart = frame / m_nLoadCount * m_nLoadCount;
delete m_pDcmImg;
unsigned long flag = CIF_UsePartialAccessToPixelData;
m_pDcmImg = new DicomImage(pDataset, m_orgXfer, flag, m_nFrameStart, m_nLoadCount);
}
m_pDcmImg->setWindow(wc, ww);
DicomImage* di = m_pDcmImg;
bool bReverse = false;
OFString Presentation;
pDataset->findAndGetOFString(DCM_PresentationLUTShape, Presentation);
EP_Polarity p = di->getPolarity();
ES_PresentationLut esp = di->getPresentationLutShape();
EP_Interpretation epi = di->getPhotometricInterpretation();
int size = 0;
if (di)
{
if ((Presentation.compare("INVERSE") == 0 && di->getPhotometricInterpretation() == EPI_Monochrome2)
|| (Presentation.compare("IDENTITY") == 0 && di->getPhotometricInterpretation() == EPI_Monochrome1))
bReverse = true;
// 负像
if (bneg)
{
if (bReverse)
di->setPolarity(EPP_Normal);
else
di->setPolarity(EPP_Reverse);
}
else
{
if (bReverse)
di->setPolarity(EPP_Reverse);
else
di->setPolarity(EPP_Normal);
}
w = di->getWidth();
h = di->getHeight();
size = di->createWindowsDIB(pdib, 0, frame-m_nFrameStart, 24, 1)
}
return (size>0);
}
bool DcmParser::Export(std::string dst, int format)
{
int result = 0;
int nFrames = GetFrameCount();
std::string sopInsUid = GetTagStringValue(0x0008, 0x0018);
std::string serInsUid = GetTagStringValue(0x0020, 0x000e);
std::string ext[] = { ".jpg", ".png", ".bmp", ".tif", ".jpg" };
std::string serDir = dst + "\\" + serInsUid;
MakePath(serDir);
std::vector<std::string> aviFiles;
int w, h;
for (int i=0; i<nFrames; i++)
{
std::string fn = serDir + "\\" + sopInsUid + "_" + std::to_string(i) + ext[format];
DicomImage *di = createImage(i);
int frame = i - m_nFrameStart;
if (i == 0) {
w = di->getWidth();
h = di->getHeight();
}
switch (format)
{
case 0: // jpg
case 4:
{
DiJPEGPlugin plugin;
plugin.setQuality(OFstatic_cast(unsigned int, 100));
plugin.setSampling(ESS_422);
result = di->writePluginFormat(&plugin, fn.c_str(), frame);
if (result && format==4)
{
aviFiles.push_back(fn);
}
}
break;
case 1: // png
{
DiPNGPlugin pngPlugin;
pngPlugin.setInterlaceType(E_pngInterlaceAdam7);
pngPlugin.setMetainfoType(E_pngFileMetainfo);
result = di->writePluginFormat(&pngPlugin, fn.c_str(), frame);
}
break;
case 2: // bmp
result = di->writeBMP(fn.c_str(), 0, frame);
break;
case 3: // tiff
{
//std::string tivVer = DiTIFFPlugin::getLibraryVersionString();
//CString msg = tivVer.c_str();
//
//OutputDebugString(msg + "\r\n");
//DiTIFFPlugin tiffPlugin;
//tiffPlugin.setCompressionType(E_tiffLZWCompression);
////tiffPlugin.setCompressionType(E_tiffPackBitsCompression);
//tiffPlugin.setLZWPredictor(E_tiffLZWPredictorDefault);
//tiffPlugin.setRowsPerStrip(OFstatic_cast(unsigned long, 0));
//result = di->writePluginFormat(&tiffPlugin, ofile, frame);
result = SaveToTiff(di, fn, frame);
}
break;
default:
break;
}
}
if (format == 4) {
if (aviFiles.size() > 9) {
std::string fn = serDir + "\\" + sopInsUid + ".mp4";
cv::VideoWriter writer;
cv::Mat cvImg;
//writer = cv::VideoWriter(fn,
// cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), 5,
// cv::Size(w, h));
writer = cv::VideoWriter(fn,
cv::VideoWriter::fourcc('M', 'P', '4', 'V'), 5,
cv::Size(w, h));
for (int i = 0; i < aviFiles.size(); i++)
{
std::string jpgFn = aviFiles.at(i);
cvImg = cv::imread(jpgFn);
writer << cvImg;
DeleteFile(jpgFn.c_str());
}
}
else {
result = 0;
}
}
return result>0;
}
int DcmParser::SaveToTiff(DicomImage* di, std::string dst, int frame)
{
void *data = OFconst_cast(void *, di->getOutputData(8, frame, 0));
if (data == nullptr) {
return 0;
}
int cols = di->getWidth();
int rows = di->getHeight();
OFBool isMono = di->isMonochrome();
short photometric = isMono ? PHOTOMETRIC_MINISBLACK : PHOTOMETRIC_RGB;
short samplesperpixel = isMono ? 1 : 3;
unsigned long bytesperrow = cols * samplesperpixel;
long opt_rowsperstrip = OFstatic_cast(long, 0);
if (opt_rowsperstrip <= 0) opt_rowsperstrip = 8192 / bytesperrow;
if (opt_rowsperstrip == 0) opt_rowsperstrip++;
TIFF* tif = TIFFOpen(dst.c_str(), "w");
if (!tif) {
return 0;
}
int ret = 0;
ret = TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, cols);
ret = TIFFSetField(tif, TIFFTAG_IMAGELENGTH, rows);
ret = TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
//ret = TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_LZW);
ret = TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_PACKBITS);
ret = TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, photometric);
ret = TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, samplesperpixel);
ret = TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, opt_rowsperstrip);
int OK = 1;
unsigned long offset = 0;
unsigned char *bytedata = OFstatic_cast(unsigned char *, data);
for (Uint16 i = 0; (i < rows) && OK; i++)
{
if (TIFFWriteScanline(tif, bytedata + offset, i, 0) < 0)
OK = 0;
offset += bytesperrow;
}
TIFFFlushData(tif);
TIFFClose(tif);
return OK;
}
DicomImage* DcmParser::createImage(int frame)
{
DcmDataset* pDataset = m_dcmFile.getDataset();
DicomImage* di = nullptr;
int fend = m_nFrameStart + m_nLoadCount - 1;
if (!(frame >= m_nFrameStart && frame <= fend))
{
m_nFrameStart = frame / m_nLoadCount * m_nLoadCount;
unsigned long flag = CIF_UsePartialAccessToPixelData;
if (m_pDcmImg) {
delete m_pDcmImg;
}
m_pDcmImg = new DicomImage(pDataset, m_orgXfer, flag, m_nFrameStart, m_nLoadCount);
}
di = m_pDcmImg;
di->setWindow(m_defWC, m_defWW);
bool bReverse = false;
OFString Presentation;
pDataset->findAndGetOFString(DCM_PresentationLUTShape, Presentation);
int size = 0;
if (di)
{
if ((Presentation.compare("INVERSE") == 0 && di->getPhotometricInterpretation() == EPI_Monochrome2)
|| (Presentation.compare("IDENTITY") == 0 && di->getPhotometricInterpretation() == EPI_Monochrome1))
bReverse = true;
if (bReverse)
di->setPolarity(EPP_Reverse);
else
di->setPolarity(EPP_Normal);
}
return di;
}
BOOL DcmParser::IsRGB()
{
return m_pDcmImg == NULL ? FALSE : m_pDcmImg->getPhotometricInterpretation() == EPI_RGB;
}
}
2. CBaseImage, CDicomImage类
简要描述
包装DcmParser类,从dicom文件中读取患者信息、检查信息、序列信息、图像信息,及由图像数据生成的位图数据。
头文件CBaseImage.h
cpp
#pragma once
#include "DcmParser.h"
#include <string>
#include <vector>
#include <mutex>
enum IMAGETYPE
{
IT_UNKNOWN,
IT_DCM,
IT_BMP,
IT_JPG,
IT_PNG,
IT_GIF,
IT_TIFF,
IT_PDF,
};
struct PatientInfo
{
CString patId; // (0010,0020) LO Patient ID
CString patName; // (0010,0010) PN Patient's Name
CString sex; // (0010,0040) CS Patient's Sex
CString birthday; // (0010,0030) DA Patient's Birth Date
CString studyAge; // (0010,1010) AS Patient's Age
float patWeight; // (0010,1030) DS Patient's Weight
};
struct StudyInfo
{
CString studyInsUid; // (0020,000D) UI Study Instance UID
CString studyDesc; // (0008,1030) LO Study Description
CString studyId; // (0020,0010) SH Study ID
CString accessionNumber; // (0008,0050) SH Accession Number
CString studyDate; // (0008,0020) DA Study Date
CString studyTime; // (0008,0030) TM Study Time
CString modality; // (0008,0060) CS Modality
CString manufacturer; // (0008,0070) LO Manufacturer
CString hospitalName; // (0008,0080) LO Institution Name
CString hospitalAddr; // (0008,0081) ST Institution Address
CString operatorName; // (0008,1070) PN Operators' Name
};
struct SeriesInfo
{
CString seriesInsUid; // (0020,000E) UI Series Instance UID
CString seriesDesc; // (0008,103E) LO Series Description
CString seriesDate; // (0008,0021) DA Series Date
CString seriesTime; // (0008,0031) TM Series Time
int seriesNumber; // (0020,0011) IS Series Number
// CT
CString kvp; // (0018,0060) DS KVP
CString exposureTime; // (0018,1150) IS Exposure Time
CString xRayTubeCurrent; // (0018,1151) IS X-Ray Tube Current
CString exposure; // (0018,1152) IS Exposure
// MR
CString percentFOV; // (0018,0094) DS Percent Phase Field of View
CString TR; // (0018,0080) DS Repetition Time;
CString TE; // (0018,0081) DS Echo Time;
CString FS; // (0018,0087) DS Magnetic Field Strength;
int amiX; // (0018,1310) US 0 Acquisition Matrix
int amiY; // (0018,1310) US 1 Acquisition Matrix
int amjX; // (0018,1310) US 2 Acquisition Matrix
int amjY; // (0018,1310) US 3 Acquisition Matrix
CString protocolName; // (0018,1030) LO Protocol Name
CString scanSequence; // (0018,0020) CS Scanning Sequence
};
struct ImageInfo
{
float ipX; // (0020,0032) DS 0 Image Position(Patient)
float ipY; // (0020,0032) DS 1 Image Position(Patient)
float ipZ; // (0020,0032) DS 2 Image Position(Patient)
float ioiX; // (0020,0037) DS 0 Image Orientation(Patient)
float ioiY; // (0020,0037) DS 1 Image Orientation(Patient)
float ioiZ; // (0020,0037) DS 2 Image Orientation(Patient)
float iojX; // (0020,0037) DS 3 Image Orientation(Patient)
float iojY; // (0020,0037) DS 4 Image Orientation(Patient)
float iojZ; // (0020,0037) DS 5 Image Orientation(Patient)
CString poX; // (0020,0020) DS 0 Patient Orientation
CString poY; // (0020,0020) DS 1 Patient Orientation
CString patPosition; // (0018,5100) CS Patient Position
CString sliceThickness; // (0018,0050) DS Slice Thickness
CString sliceLocation; // (0020,1041) DS Slice Location
int samplePerPixel; // (0028,0002) US Samples per Pixel
float psX; // (0028,0030) DS 0 Pixel Spacing
float psY; // (0028,0030) DS 1 Pixel Spacing
int bitsAlloc; // (0028,0100) US Bits Allocated
int bitsStored; // (0028,0101) US Bits Stored
int hightBit; // (0028,0102) US High Bit
int pixelRep; // (0028,0103) US Pixel Representation
float defWinCenter; // (0028,1050) DS Window Center
float defWinWidth; // (0028,1051) DS Window Width
float rescaleIntercept; // (0028,1052) DS Rescale Intercept
float rescaleSlope; // (0028,1053) DS Rescale Slope
int height; // (0028,0010) US Rows
int width; // (0028,0011) US Columns
int instanceNumber; // (0020,0013) IS Instance Number
CString sopInsUid; // (0008,0018) UI SOP Instance UID
CString xfer; // (0002,0010) UI Transfer Syntax UID
CString imageType; // (0008,0008) CS Image Type
CString charset; // (0008,0005) CS Specific Character Set
CString contentDate; // (0008,0023) DA Content Date
CString contentTime; // (0008,0033) TM Content Time
CString acquisitionDate; // (0008,0022) DA Acquisition Date
CString acquisitionTime; // (0008,0032) TM Acquisition Time
};
class CBaseImage
{
public:
CBaseImage();
virtual ~CBaseImage();
public:
PatientInfo& GetPatientInfo();
StudyInfo& GetStudyInfo();
SeriesInfo& GetSeriesInfo();
ImageInfo& GetImageInfo();
virtual BOOL ParseFile(std::string fn)=0;
virtual void Close() = 0;
virtual BOOL ReParseFile() = 0;
virtual void GetDefaultWindow(double& wc, double& ww) = 0;
virtual BOOL CreateDIB(void*& pdib, int& w, int& h, int wc, int ww, int frame = 0, bool bneg = false) = 0;
virtual std::string GetFileName() { return m_fileName; }
virtual BOOL IsValid() { return m_bParserValid; }
virtual int GetFrameCount() { return 1; }
virtual bool Export(std::string dst, int format) {
return false;
}
DcmParser* GetParser() { return &m_parser; }
protected:
PatientInfo m_patInfo;
StudyInfo m_studyInfo;
SeriesInfo m_seriesInfo;
ImageInfo m_imageInfo;
BOOL m_bParserValid;
std::string m_fileName;
DcmParser m_parser;
};
源文件CBaseImage.cpp
cpp
#include "pch.h"
#include "CBaseImage.h"
CBaseImage::CBaseImage()
: m_bParserValid(FALSE)
{
}
CBaseImage::~CBaseImage()
{
}
PatientInfo& CBaseImage::GetPatientInfo()
{
return m_patInfo;
}
StudyInfo& CBaseImage::GetStudyInfo()
{
return m_studyInfo;
}
SeriesInfo& CBaseImage::GetSeriesInfo()
{
return m_seriesInfo;
}
ImageInfo& CBaseImage::GetImageInfo()
{
return m_imageInfo;
}
头文件CDicomImage.h
cpp
#pragma once
#include "CBaseImage.h"
class CDicomImage : public CBaseImage
{
public:
CDicomImage();
virtual ~CDicomImage();
mutable std::mutex m_mtx;
mutable std::mutex m_parsermtx;
public:
BOOL ParseFile(std::string fn);
void GetDefaultWindow(double& wc, double& ww);
BOOL CreateDIB(void*& pdib, int& w, int& h, int wc, int ww, int frame=0, bool bneg = false, bool bov = true);
virtual int GetFrameCount();
virtual void Close();
virtual BOOL ReParseFile();
bool Export(std::string dst, int format);
};
源文件CDicomImage.cpp
cpp
#include "pch.h"
#include "CDicomImage.h"
#include "Utilities.h"
CDicomImage::CDicomImage()
{
}
CDicomImage::~CDicomImage()
{
}
BOOL CDicomImage::ParseFile(std::string fn)
{
std::lock_guard<std::mutex> lg(m_mtx);
if (!m_parser.Open(fn))
return FALSE;
CString charset = m_parser.GetTagValue(0x0008, 0x0005);
BOOL needTransToGbk = FALSE;
if (charset == _T("ISO_IR 192"))
needTransToGbk = TRUE;
CString tmp;
std::string strVal;
// Patient
m_patInfo.patId = m_parser.GetTagValue(0x0010, 0x0020);
strVal = m_parser.GetTagStringValue(0x0010, 0x0010);
if (needTransToGbk)
m_patInfo.patName = UTF8toA(strVal).c_str();
else
m_patInfo.patName = strVal.c_str();
m_patInfo.sex = m_parser.GetTagValue(0x0010, 0x0040);
tmp = m_parser.GetTagValue(0x0010, 0x0030);
if (tmp.GetLength() == 8)
{
m_patInfo.birthday = tmp.Left(4);
m_patInfo.birthday += _T("-");
m_patInfo.birthday += tmp.Mid(4, 2);
m_patInfo.birthday += _T("-");
m_patInfo.birthday += tmp.Mid(6);
}
else
m_patInfo.birthday = tmp;
m_patInfo.studyAge = m_parser.GetTagValue(0x0010, 0x1010);
m_patInfo.patWeight = m_parser.GetTagFloatValue(0x0010, 0x1030);
// Study
m_studyInfo.modality = m_parser.GetTagValue(0x0008, 0x0060);
m_studyInfo.studyInsUid = m_parser.GetTagValue(0x0020, 0x000D);
strVal = m_parser.GetTagStringValue(0x0008, 0x1030);
if (needTransToGbk)
m_studyInfo.studyDesc = UTF8toA(strVal).c_str();
else
m_studyInfo.studyDesc = strVal.c_str();
m_studyInfo.studyId = m_parser.GetTagValue(0x0020, 0x0010);
m_studyInfo.accessionNumber = m_parser.GetTagValue(0x0008, 0x0050);
tmp = m_parser.GetTagValue(0x0008, 0x0020);
if (tmp.GetLength() == 8)
{
m_studyInfo.studyDate = tmp.Left(4);
m_studyInfo.studyDate += _T("-");
m_studyInfo.studyDate += tmp.Mid(4, 2);
m_studyInfo.studyDate += _T("-");
m_studyInfo.studyDate += tmp.Mid(6);
}
else
m_studyInfo.studyDate = tmp;
tmp = m_parser.GetTagValue(0x0008, 0x0030);
if (tmp.GetLength() >= 6)
{
m_studyInfo.studyTime = tmp.Left(2);
m_studyInfo.studyTime += _T(":");
m_studyInfo.studyTime += tmp.Mid(2, 2);
m_studyInfo.studyTime += _T(":");
m_studyInfo.studyTime += tmp.Mid(4);
}
else
m_studyInfo.studyTime = tmp;
strVal = m_parser.GetTagStringValue(0x0008, 0x0070);
if (needTransToGbk)
m_studyInfo.manufacturer = UTF8toA(strVal).c_str();
else
m_studyInfo.manufacturer = strVal.c_str();
strVal = m_parser.GetTagStringValue(0x0008, 0x0080);
if (needTransToGbk)
m_studyInfo.hospitalName = UTF8toA(strVal).c_str();
else
m_studyInfo.hospitalName = strVal.c_str();
strVal = m_parser.GetTagStringValue(0x0008, 0x0081);
if (needTransToGbk)
m_studyInfo.hospitalAddr = UTF8toA(strVal).c_str();
else
m_studyInfo.hospitalAddr = strVal.c_str();
strVal = m_parser.GetTagStringValue(0x0008, 0x1070);
if (needTransToGbk)
m_studyInfo.operatorName = UTF8toA(strVal).c_str();
else
m_studyInfo.operatorName = strVal.c_str();
// Series
m_seriesInfo.seriesInsUid = m_parser.GetTagValue(0x0020, 0x000E);
strVal = m_parser.GetTagStringValue(0x0008, 0x103E);
if (needTransToGbk)
m_seriesInfo.seriesDesc = UTF8toA(strVal).c_str();
else
m_seriesInfo.seriesDesc = strVal.c_str();
tmp = m_parser.GetTagValue(0x0008, 0x0021);
if (tmp.GetLength() == 8)
{
m_seriesInfo.seriesDate = tmp.Left(4);
m_seriesInfo.seriesDate += _T("-");
m_seriesInfo.seriesDate += tmp.Mid(4, 2);
m_seriesInfo.seriesDate += _T("-");
m_seriesInfo.seriesDate += tmp.Mid(6);
}
else
m_seriesInfo.seriesDate = tmp;
tmp = m_parser.GetTagValue(0x0008, 0x0031);
if (tmp.GetLength() >= 6)
{
m_seriesInfo.seriesTime = tmp.Left(2);
m_seriesInfo.seriesTime += _T(":");
m_seriesInfo.seriesTime += tmp.Mid(2, 2);
m_seriesInfo.seriesTime += _T(":");
m_seriesInfo.seriesTime += tmp.Mid(4);
}
else
m_seriesInfo.seriesTime = tmp;
m_seriesInfo.seriesNumber = m_parser.GetTagIntValue(0x0020, 0x0011);
// CT
m_seriesInfo.kvp = m_parser.GetTagValue(0x0018, 0x0060);
m_seriesInfo.exposureTime = m_parser.GetTagValue(0x0018, 0x1150);
m_seriesInfo.xRayTubeCurrent = m_parser.GetTagValue(0x0018, 0x1151);
m_seriesInfo.exposure = m_parser.GetTagValue(0x0018, 0x1152);
// MR
m_seriesInfo.percentFOV = m_parser.GetTagValue(0x0018, 0x0094);
m_seriesInfo.TR = m_parser.GetTagValue(0x0018, 0x0080);
m_seriesInfo.TE = m_parser.GetTagValue(0x0018, 0x0081);
m_seriesInfo.FS = m_parser.GetTagValue(0x0018, 0x0087);
m_seriesInfo.amiX = m_parser.GetTagIntValue(0x0018, 0x1310, 0);
m_seriesInfo.amiY = m_parser.GetTagIntValue(0x0018, 0x1310, 1);
m_seriesInfo.amjX = m_parser.GetTagIntValue(0x0018, 0x1310, 2);
m_seriesInfo.amjY = m_parser.GetTagIntValue(0x0018, 0x1310, 3);
m_seriesInfo.protocolName = m_parser.GetTagValue(0x0018, 0x1030);
m_seriesInfo.scanSequence = m_parser.GetTagValue(0x0018, 0x0020);
// Image
m_imageInfo.ipX = m_parser.GetTagFloatValue(0x0020, 0x0032, 0);
m_imageInfo.ipY = m_parser.GetTagFloatValue(0x0020, 0x0032, 1);
m_imageInfo.ipZ = m_parser.GetTagFloatValue(0x0020, 0x0032, 2);
m_imageInfo.ioiX = m_parser.GetTagFloatValue(0x0020, 0x0037, 0);
m_imageInfo.ioiY = m_parser.GetTagFloatValue(0x0020, 0x0037, 1);
m_imageInfo.ioiZ = m_parser.GetTagFloatValue(0x0020, 0x0037, 2);
m_imageInfo.iojX = m_parser.GetTagFloatValue(0x0020, 0x0037, 3);
m_imageInfo.iojY = m_parser.GetTagFloatValue(0x0020, 0x0037, 4);
m_imageInfo.iojZ = m_parser.GetTagFloatValue(0x0020, 0x0037, 5);
m_imageInfo.poX = m_parser.GetTagValue(0x0020, 0x0020, 0);
m_imageInfo.poY = m_parser.GetTagValue(0x0020, 0x0020, 1);
m_imageInfo.patPosition = m_parser.GetTagValue(0x0018, 0x5100);
m_imageInfo.sliceThickness = m_parser.GetTagValue(0x0018, 0x0050);
m_imageInfo.sliceLocation = m_parser.GetTagValue(0x0020, 0x1041);
m_imageInfo.samplePerPixel = m_parser.GetTagIntValue(0x0028, 0x0002);
m_imageInfo.psX = m_parser.GetTagFloatValue(0x0018, 0x1164, 0);
m_imageInfo.psY = m_parser.GetTagFloatValue(0x0018, 0x1164, 1);
if (m_imageInfo.psX == 0.0 || m_imageInfo.psY == 0.0) {
m_imageInfo.psX = m_parser.GetTagFloatValue(0x0028, 0x0030, 0);
m_imageInfo.psY = m_parser.GetTagFloatValue(0x0028, 0x0030, 1);
}
m_imageInfo.bitsAlloc = m_parser.GetTagIntValue(0x0028, 0x0100);
m_imageInfo.bitsStored = m_parser.GetTagIntValue(0x0028, 0x0101);
m_imageInfo.hightBit = m_parser.GetTagIntValue(0x0028, 0x0102);
m_imageInfo.pixelRep = m_parser.GetTagIntValue(0x0028, 0x0103);
m_imageInfo.defWinCenter = m_parser.GetTagFloatValue(0x0028, 0x1050);
m_imageInfo.defWinWidth = m_parser.GetTagFloatValue(0x0028, 0x1051);
if (m_imageInfo.defWinCenter==0.0 && m_imageInfo.defWinWidth==0.0)
{
}
m_imageInfo.rescaleIntercept = m_parser.GetTagFloatValue(0x0028, 0x1052);
m_imageInfo.rescaleSlope = m_parser.GetTagFloatValue(0x0028, 0x1053);
m_imageInfo.height = m_parser.GetTagIntValue(0x0028, 0x0010);
m_imageInfo.width = m_parser.GetTagIntValue(0x0028, 0x0011);
m_imageInfo.instanceNumber = m_parser.GetTagIntValue(0x0020, 0x0013);
m_imageInfo.sopInsUid = m_parser.GetTagValue(0x0008, 0x0018);
m_imageInfo.xfer = m_parser.GetTagValue(0x0002, 0x0010);
m_imageInfo.imageType = m_parser.GetTagValue(0x0008, 0x0008);
m_imageInfo.charset = charset;
tmp = m_parser.GetTagValue(0x0008, 0x0023);
if (tmp.GetLength() == 8)
{
m_imageInfo.contentDate = tmp.Left(4);
m_imageInfo.contentDate += _T("-");
m_imageInfo.contentDate += tmp.Mid(4, 2);
m_imageInfo.contentDate += _T("-");
m_imageInfo.contentDate += tmp.Mid(6);
}
else
m_imageInfo.contentDate = tmp;
tmp = m_parser.GetTagValue(0x0008, 0x0033);
if (tmp.GetLength() >= 6)
{
m_imageInfo.contentTime = tmp.Left(2);
m_imageInfo.contentTime += _T(":");
m_imageInfo.contentTime += tmp.Mid(2, 2);
m_imageInfo.contentTime += _T(":");
m_imageInfo.contentTime += tmp.Mid(4);
}
else
m_imageInfo.contentTime = tmp;
tmp = m_parser.GetTagValue(0x0008, 0x0022);
if (tmp.GetLength() == 8)
{
m_imageInfo.acquisitionDate = tmp.Left(4);
m_imageInfo.acquisitionDate += _T("-");
m_imageInfo.acquisitionDate += tmp.Mid(4, 2);
m_imageInfo.acquisitionDate += _T("-");
m_imageInfo.acquisitionDate += tmp.Mid(6);
}
else
m_imageInfo.acquisitionDate = tmp;
tmp = m_parser.GetTagValue(0x0008, 0x0032);
if (tmp.GetLength() >= 6)
{
m_imageInfo.acquisitionTime = tmp.Left(2);
m_imageInfo.acquisitionTime += _T(":");
m_imageInfo.acquisitionTime += tmp.Mid(2, 2);
m_imageInfo.acquisitionTime += _T(":");
m_imageInfo.acquisitionTime += tmp.Mid(4);
}
else
m_imageInfo.acquisitionTime = tmp;
if (m_seriesInfo.seriesDate.IsEmpty())
m_seriesInfo.seriesDate = m_imageInfo.acquisitionDate;
if (m_seriesInfo.seriesTime.IsEmpty())
m_seriesInfo.seriesTime = m_imageInfo.acquisitionTime;
m_bParserValid = m_parser.HasImage();
m_fileName = fn;
Close();
return TRUE;
}
void CDicomImage::GetDefaultWindow(double& wc, double& ww)
{
m_parser.GetDefaultWindow(wc, ww);
}
BOOL CDicomImage::CreateDIB(void*& pdib, int& w, int& h, int wc, int ww, int frame/* = 0*/, bool bneg/* = false*/)
{
if (pdib)
{
delete[]pdib;
pdib = NULL;
}
if (!m_bParserValid)
ReParseFile();
return m_parser.CreateDIB(pdib, w, h, wc, ww, frame, bneg);
}
int CDicomImage::GetFrameCount()
{
return m_parser.GetFrameCount();
}
void CDicomImage::Close()
{
std::lock_guard<std::mutex> lg(m_parsermtx);
if (m_parser.GetFrameCount() > 1)
return;
m_parser.Close();
m_nImgSize = 0;
m_bParserValid = FALSE;
}
BOOL CDicomImage::ReParseFile()
{
std::lock_guard<std::mutex> lg(m_parsermtx);
if (m_bParserValid)
return TRUE;
if (m_fileName.empty())
return FALSE;
if (!m_parser.Open(m_fileName))
return FALSE;
m_nImgSize = m_parser.GetImageSize();
//m_bParserValid = m_parser.HasImage();
m_bParserValid = m_parser.IsValid();
return m_bParserValid;
}
bool CDicomImage::Export(std::string dst, int format)
{
if (!IsValid()) ReParseFile();
return m_parser.Export(dst, format);
}
3. Displayer类
简要描述
从CWnd派生的窗口类,包含CBaseImage读取dicom文件信息、生成位图数据,包含DrawParam保存绘制参数。响应WM_PAINT消息,调用DrawImage绘制图像, 调用DrawText绘制文字
后续章节再添加鼠标滚轮消息实现滚动多帧图,鼠标按下消息、鼠标移动消息实现图像缩放、图像移动、调窗等功能
DrawImage 中使用Gdiplus::Graphics DrawImage绘制图像
DrawText 中使用Gdiplus::Graphics MeasureString获取文字区域, DrawString绘制文字
头文件Displayer.h
cpp
#pragma once
#include "DrawParam.h"
// Displayer
class CBaseImage;
class Displayer : public CWnd
{
DECLARE_DYNAMIC(Displayer)
public:
Displayer();
Displayer(CWnd* pParent);
virtual ~Displayer();
bool LoadFile(const char* fn);
void SetImage(CBaseImage* pImg);
void PrevFrame();
void NextFrame();
int GetFrameCount();
// format = 0 jpg, 1 png, 2 tiff
bool Export(std::string dst, int format);
protected:
DECLARE_MESSAGE_MAP()
afx_msg void OnPaint();
afx_msg void OnSize(UINT nType, int cx, int cy);
void FitToView();
void DrawImage(Gdiplus::Graphics& gs, void* pDib, CRect rcDraw, DrawParam& param);
void DrawText(Gdiplus::Graphics& gs);
private:
CBaseImage* m_pImage;
void* m_pDib;
DrawParam m_drawParam;
};
源文件Displayer.cpp
cpp
// Displayer.cpp: 实现文件
//
#include "pch.h"
#include "DcmImage.h"
#include "Displayer.h"
#include "CDicomImage.h"
#include "Utilities.h"
// Displayer
IMPLEMENT_DYNAMIC(Displayer, CWnd)
Displayer::Displayer()
: m_pImage(nullptr)
, m_pDib(nullptr)
{
}
Displayer::Displayer(CWnd* pParent/*=nullptr*/)
: m_pImage(nullptr)
, m_pDib(nullptr)
{
}
Displayer::~Displayer()
{
}
BEGIN_MESSAGE_MAP(Displayer, CWnd)
ON_WM_PAINT()
ON_WM_SIZE()
END_MESSAGE_MAP()
// Displayer 消息处理程序
void Displayer::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CWnd::OnPaint()
// 1. 获取客户区大小
CRect rect;
GetClientRect(&rect);
int nWidth = rect.Width();
int nHeight = rect.Height();
// 2. 创建内存 DC 和临时位图(双缓冲载体)
CDC memDC;
memDC.CreateCompatibleDC(&dc); // 创建与屏幕 DC 兼容的内存 DC
CBitmap bmp;
bmp.CreateCompatibleBitmap(&dc, nWidth, nHeight); // 创建与客户区等大的位图
CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 将位图选入内存 DC
memDC.FillSolidRect(rect, RGB(0, 0, 0));
// 4. 使用 GDI+ 在内存 DC 上绘制图像
Graphics gs(memDC.GetSafeHdc()); // 关联内存 DC 到 GDI+
DrawImage(gs, m_pDib, rect, m_drawParam);
// 5. 将内存 DC 内容一次性复制到屏幕 DC(避免闪烁)
dc.BitBlt(0, 0, nWidth, nHeight, &memDC, 0, 0, SRCCOPY);
// 6. 清理资源
memDC.SelectObject(pOldBmp);
}
void Displayer::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
// TODO: 在此处添加消息处理程序代码
if (m_pImage) {
FitToView();
}
}
bool Displayer::LoadFile(const char* fn)
{
CDicomImage* pImg = new CDicomImage();
if (!pImg->ParseFile(fn)) {
delete pImg;
pImg = nullptr;
return false;
}
SetImage(pImg);
return true;
}
void Displayer::SetImage(CBaseImage* pImg)
{
if (m_pImage) {
delete m_pImage;
m_pImage = nullptr;
}
m_pImage = pImg;
if (m_pDib)
{
delete[] m_pDib;
m_pDib = nullptr;
}
m_drawParam.Clear();
m_pImage->GetDefaultWindow(m_drawParam.winCenter, m_drawParam.winWidth);
m_pImage->CreateDIB(m_pDib, m_drawParam.width, m_drawParam.height, m_drawParam.winCenter,
m_drawParam.winWidth, m_drawParam.nFrame, m_drawParam.bNagtive, true);
FitToView();
}
void Displayer::PrevFrame()
{
int nFrame = m_drawParam.nFrame - 1;
if (nFrame < 0) {
return;
}
m_drawParam.nFrame = nFrame;
if (m_pDib)
{
delete[] m_pDib;
m_pDib = nullptr;
}
m_pImage->CreateDIB(m_pDib, m_drawParam.width, m_drawParam.height, m_drawParam.winCenter,
m_drawParam.winWidth, m_drawParam.nFrame, m_drawParam.bNagtive, true);
FitToView();
}
void Displayer::NextFrame()
{
int nFrameCount = m_pImage->GetFrameCount();
int nFrame = m_drawParam.nFrame + 1;
if (nFrame > nFrameCount - 1) {
return;
}
m_drawParam.nFrame = nFrame;
if (m_pDib)
{
delete[] m_pDib;
m_pDib = nullptr;
}
m_pImage->CreateDIB(m_pDib, m_drawParam.width, m_drawParam.height, m_drawParam.winCenter,
m_drawParam.winWidth, m_drawParam.nFrame, m_drawParam.bNagtive, true);
FitToView();
}
int Displayer::GetFrameCount()
{
if (m_pImage == nullptr)
return 0;
return m_pImage->GetFrameCount();
}
void Displayer::FitToView()
{
if (m_pImage == nullptr)
return;
CRect rc;
GetClientRect(&rc);
int w = rc.Width();
int h = rc.Height();
int imgW = m_drawParam.width;
int imgH = m_drawParam.height;
float fx = w * 0.9 / imgW;
float fy = h * 0.9 / imgH;
float f = min(fx, fy);
if (f < 0.02) f = 0.02;
m_drawParam.zoom = f;
Invalidate();
}
void Displayer::DrawImage(Gdiplus::Graphics& gs, void* pDib, CRect rcDraw, DrawParam& param)
{
Gdiplus::Bitmap* img = nullptr;
if (pDib)
{
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(BITMAPINFO));
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = param.width;
bmi.bmiHeader.biHeight = param.height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 24;
bmi.bmiHeader.biCompression = BI_RGB;
bmi.bmiHeader.biClrImportant = 0;
bmi.bmiHeader.biClrUsed = 0;
img = new Gdiplus::Bitmap(&bmi, pDib);
}
// 图像
if (img)
{
Rect rcDest;
rcDest.X = rcDest.Y = 0;
rcDest.Width = param.width;
rcDest.Height = param.height;
Matrix* mtx = param.GetMatrix(rcDraw);
gs.SetTransform(mtx);
gs.DrawImage(img, rcDest);
gs.ResetTransform();
DrawText(gs);
delete img;
img = nullptr;
}
}
void Displayer::DrawText(Gdiplus::Graphics& gs)
{
CRect rc;
GetClientRect(&rc);
Gdiplus::Font font(L"微软雅黑", 12);
SolidBrush brush(Color(255, 255, 255));
Gdiplus::PointF pt(2, 2);
Gdiplus::RectF bound;
int txtH = 18;
// 左上角
CString txt;
txt = m_pImage->GetPatientInfo().patName;
if (m_pImage->GetPatientInfo().sex != _T("")) {
txt += +_T(" / ") + m_pImage->GetPatientInfo().sex;
}
if (m_pImage->GetPatientInfo().studyAge != _T("")) {
txt += +_T(" / ") + m_pImage->GetPatientInfo().studyAge;
}
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
if (m_pImage->GetStudyInfo().studyId != _T("")) {
pt.Y += txtH;
txt = m_pImage->GetStudyInfo().studyId;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
// 右上角
pt.Y = 2;
txt.Format(_T("WL: %.f WW: %.f"), m_drawParam.winCenter, m_drawParam.winWidth);
gs.MeasureString(AtoW(txt).c_str(), -1, &font, PointF(0, 0), &bound);
pt.X = rc.right - bound.Width + 2;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
int nFrames = m_pImage->GetFrameCount();
if (nFrames > 1) {
txt.Format(_T("fr: %d/%d"), m_drawParam.nFrame + 1, nFrames);
gs.MeasureString(AtoW(txt).c_str(), -1, &font, PointF(0, 0), &bound);
pt.Y += txtH;
pt.X = rc.right - bound.Width - 2;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
// 左下角
pt.X = 2;
pt.Y = rc.bottom - 4;
if (m_pImage->GetStudyInfo().hospitalName != _T("")) {
pt.Y -= txtH;
txt = m_pImage->GetStudyInfo().hospitalName;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
if (m_pImage->GetSeriesInfo().seriesDate != _T("")) {
pt.Y -= txtH;
txt = m_pImage->GetSeriesInfo().seriesDate;
if (m_pImage->GetSeriesInfo().seriesTime != _T("")) {
txt += _T(" ") + m_pImage->GetSeriesInfo().seriesTime;
}
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
if (m_pImage->GetSeriesInfo().seriesDesc != _T("")) {
pt.Y -= txtH;
txt = m_pImage->GetSeriesInfo().seriesDesc;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
if (m_pImage->GetStudyInfo().modality != _T("")) {
pt.Y -= txtH;
txt = m_pImage->GetStudyInfo().modality;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
// 右下角
pt.Y = rc.bottom - 4;
txt = _T("");
CString tmp;
tmp = m_pImage->GetImageInfo().sliceThickness;
if (!tmp.IsEmpty()) {
txt = _T("T:") + tmp;
}
tmp = m_pImage->GetImageInfo().sliceLocation;
if (!tmp.IsEmpty()) {
if (!txt.IsEmpty()) txt += _T(" ");
double location = atof(tmp);
tmp.Format(_T("%.1f"), location);
txt += _T("L:") + tmp;
}
gs.MeasureString(AtoW(txt).c_str(), -1, &font, PointF(0, 0), &bound);
pt.Y -= txtH;
pt.X = rc.right - bound.Width - 2;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
txt.Format(_T("Zoom: %.2f"), m_drawParam.zoom);
gs.MeasureString(AtoW(txt).c_str(), -1, &font, PointF(0, 0), &bound);
pt.Y -= txtH;
pt.X = rc.right - bound.Width - 2;
gs.DrawString(AtoW(txt).c_str(), -1, &font, pt, &brush);
}
bool Displayer::Export(std::string dst, int format)
{
if (m_pImage == nullptr) return false;
return m_pImage->Export(dst, format);
}
BOOL Displayer::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
if (!m_pImage) return true;
int nFrames = m_pImage->GetFrameCount();
if (nFrames > 1) {
if (zDelta < 0) {
NextFrame();
}
else {
PrevFrame();
}
}
return CWnd::OnMouseWheel(nFlags, zDelta, pt);
}
4. DrawParam类
简要描述
保存绘制参数,包含以下参数:
- width,height 图像宽高
- winCenter,winWidth 当前窗值
- zoom 当前缩放系数
- nOffsetX, nOffsetY 图像移动偏移
- nFrame 当前帧数
- bNagtive 是否负像
- flipRotate 旋转翻转
- matrix Gdiplus::Matrix变换矩阵
头文件DrawParam.h
cpp
#pragma once
#include "DcmImage.h"
#include <string>
#include <vector>
class DrawParam
{
public:
int width;
int height;
double winCenter;
double winWidth;
float zoom;
int nOffsetX;
int nOffsetY;
int nFrame;
bool bNagtive;
Gdiplus::Matrix matrix;
void Clear() {
zoom = 1.0;
nOffsetX = 0;
nOffsetY = 0;
nFrame = 0;
bNagtive = false;
}
struct Trans
{
enum
{
ROTATE,
FLIPH,
FLIPV,
};
int TransType;
int Angle;
};
std::vector<Trans> flipRotate;
DrawParam()
{
zoom = 1.0;
nOffsetX = 0;
nOffsetY = 0;
nFrame = 0;
bNagtive = false;
}
DrawParam& operator=(const DrawParam& ref) {
this->zoom = ref.zoom;
this->height = ref.height;
this->width = ref.width;
this->winCenter = ref.winCenter;
this->winWidth = ref.winWidth;
this->nFrame = ref.nFrame;
this->nOffsetX = ref.nOffsetX;
this->nOffsetY = ref.nOffsetY;
this->flipRotate = ref.flipRotate;
this->nFrame = ref.nFrame;
this->bNagtive = ref.bNagtive;
return *this;
}
void Reset()
{
zoom = 1.0;
nOffsetX = 0;
nOffsetY = 0;
nFrame = 0;
bNagtive = false;
flipRotate.clear();
}
void AddTrans(int type, int degree = 0)
{
Trans op;
op.TransType = type;
op.Angle = degree;
if (flipRotate.empty())
{
flipRotate.push_back(op);
}
else
{
Trans& last = flipRotate.at(flipRotate.size() - 1);
if (last.TransType != type)
{
flipRotate.push_back(op);
}
else
{
if (type == Trans::FLIPH || type == Trans::FLIPV)
{
flipRotate.pop_back();
}
else if (type == Trans::ROTATE)
{
last.Angle += degree;
last.Angle %= 360;
if (last.Angle == 0)
{
flipRotate.pop_back();
}
}
}
}
}
void ClearTrans()
{
flipRotate.clear();
}
Gdiplus::Matrix* GetMatrix(RECT rcDraw) {
int rcW = rcDraw.right - rcDraw.left;
int rcH = rcDraw.bottom - rcDraw.top;
int dw = width * zoom;
int dh = height * zoom;
int dx = (rcW - dw) / 2 + rcDraw.left;
int dy = (rcH - dh) / 2 + rcDraw.top;
int cx = width / 2;
int cy = height / 2;
int reverse = 0;
matrix.Reset();
matrix.Translate(nOffsetX + dx, nOffsetY + dy);
matrix.Scale(zoom, zoom);
for (size_t i = 0; i < flipRotate.size(); i++)
{
Trans& op = flipRotate.at(i);
if (op.TransType == Trans::FLIPH) {
if (reverse) {
matrix.Translate(0, cy);
matrix.Scale(1, -1);
matrix.Translate(0, -cy);
reverse = 0;
}
else {
matrix.Translate(cx, 0);
matrix.Scale(-1, 1);
matrix.Translate(-cx, 0);
}
}
else if (op.TransType == Trans::FLIPV) {
if (reverse) {
matrix.Translate(cx, 0);
matrix.Scale(-1, 1);
matrix.Translate(-cx, 0);
reverse = 0;
}
else {
matrix.Translate(0, cy);
matrix.Scale(1, -1);
matrix.Translate(0, -cy);
}
}
else if (op.TransType == Trans::ROTATE)
{
reverse = (op.Angle / 90) % 2;
matrix.RotateAt(op.Angle, Gdiplus::PointF(cx, cy));
}
}
return &matrix;
}
};
5. CDcmImageDlg类
简要描述
用户界面,包含文件加载区,图像显示区,操作按钮区
其中图像显示区为CStatic控件,并添加变量关联到Displayer类。需要把CStatic控件的Notify属性设置为true,以使其能响应鼠标消息。
头文件DcmImageDlg.h
cpp
// DcmImageDlg.h: 头文件
//
#pragma once
#include "Displayer.h"
// CDcmImageDlg 对话框
class CDcmImageDlg : public CDialogEx
{
// 构造
public:
CDcmImageDlg(CWnd* pParent = nullptr); // 标准构造函数
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DCMIMAGE_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
HICON m_hIcon;
// 生成的消息映射函数
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnBnClickedButtonFile();
afx_msg void OnBnClickedButtonDir();
afx_msg void OnBnClickedButtonClearfile();
afx_msg void OnSelchangeListDcm();
afx_msg void OnBnClickedButtonPrev();
afx_msg void OnBnClickedButtonNext();
DECLARE_MESSAGE_MAP()
private:
void Arrange();
void UpdateWidth(LPCTSTR lpszItem);
void EnumFileInDir(CString dir, std::vector<CString>& vec, LPCTSTR ext = NULL);
CString SelectFolder();
CListBox m_list;
Displayer m_disp;
CString m_lastDir;
std::vector<CString> m_vecDcmFile;
int m_nListWidth;
int m_nDefWidth;
};
源文件DcmImageDlg.cpp
cpp
// DcmImageDlg.cpp: 实现文件
//
#include "pch.h"
#include "framework.h"
#include "DcmImage.h"
#include "DcmImageDlg.h"
#include "afxdialogex.h"
#include "Utilities.h"
#include "DcmParser.h"
#include <shlobj.h> // 包含SHBrowseForFolder等声明
#pragma comment(lib, "shell32.lib") // 链接shell32库
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 用于应用程序"关于"菜单项的 CAboutDlg 对话框
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CDcmImageDlg 对话框
CDcmImageDlg::CDcmImageDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_DCMIMAGE_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CDcmImageDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_DCM, m_list);
DDX_Control(pDX, IDC_STATIC_DISPLAY, m_disp);
}
BEGIN_MESSAGE_MAP(CDcmImageDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON_FILE, &CDcmImageDlg::OnBnClickedButtonFile)
ON_BN_CLICKED(IDC_BUTTON_DIR, &CDcmImageDlg::OnBnClickedButtonDir)
ON_BN_CLICKED(IDC_BUTTON_CLEARFILE, &CDcmImageDlg::OnBnClickedButtonClearfile)
ON_LBN_SELCHANGE(IDC_LIST_DCM, &CDcmImageDlg::OnSelchangeListDcm)
ON_BN_CLICKED(IDC_BUTTON_PREV, &CDcmImageDlg::OnBnClickedButtonPrev)
ON_BN_CLICKED(IDC_BUTTON_NEXT, &CDcmImageDlg::OnBnClickedButtonNext)
ON_WM_SIZE()
END_MESSAGE_MAP()
// CDcmImageDlg 消息处理程序
BOOL CDcmImageDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将"关于..."菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != nullptr)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
DcmParser::RegistryCodecs();
m_nListWidth = m_nDefWidth = m_list.GetHorizontalExtent();
Arrange();
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
void CDcmImageDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialogEx::OnSysCommand(nID, lParam);
}
}
// 如果向对话框添加最小化按钮,则需要下面的代码
// 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序,
// 这将由框架自动完成。
void CDcmImageDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CDcmImageDlg::OnQueryDragIcon()
{
return static_cast<HCURSOR>(m_hIcon);
}
void CDcmImageDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
// TODO: 在此处添加消息处理程序代码
if (::IsWindow(m_list.m_hWnd)) {
Arrange();
}
}
void CDcmImageDlg::OnBnClickedButtonFile()
{
CString strFilter;
strFilter = _T("DCM file(*.dcm;*.dic)|*.dcm;*.dic|All files(*.*)|*.*||");
CFileDialog dlg(TRUE, NULL, NULL,
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_ALLOWMULTISELECT,
strFilter, NULL);
TCHAR *pszFile = new TCHAR[MAX_PATH * 500];
memset(pszFile, 0, sizeof(TCHAR)*MAX_PATH * 500);
dlg.m_ofn.lpstrFile = pszFile;
dlg.m_ofn.nMaxFile = MAX_PATH * 500;
dlg.m_ofn.lpstrFile[0] = '\0';
if (dlg.DoModal() == IDOK)
{
CString strFile;
POSITION pos = dlg.GetStartPosition();
while (pos != NULL)
{
strFile = dlg.GetNextPathName(pos);
m_vecDcmFile.push_back(strFile);
m_list.AddString(strFile);
UpdateWidth(strFile);
}
CString txt;
txt.Format(_T("DICOM文件 数量: %d"), m_list.GetCount());
GetDlgItem(IDC_STATIC_FILE)->SetWindowText(txt);
}
delete[] pszFile;
}
void CDcmImageDlg::OnBnClickedButtonDir()
{
CFolderPickerDialog dlg(m_lastDir);
if (dlg.DoModal() == IDOK)
{
CString dir = dlg.GetPathName();
m_lastDir = dir;
if (m_lastDir.Right(1) != _T("\\"))
m_lastDir += _T("\\");
EnumFileInDir(m_lastDir, m_vecDcmFile, _T(".dcm"));
for (int i = 0; i < m_vecDcmFile.size(); i++)
{
m_list.AddString(m_vecDcmFile.at(i));
UpdateWidth(m_vecDcmFile.at(i));
}
CString txt;
txt.Format(_T("DICOM文件 数量: %d"), m_list.GetCount());
GetDlgItem(IDC_STATIC_FILE)->SetWindowText(txt);
}
}
void CDcmImageDlg::OnBnClickedButtonClearfile()
{
m_list.ResetContent();
m_list.SetHorizontalExtent(m_nDefWidth);
m_nListWidth = m_nDefWidth;
m_vecDcmFile.clear();
CString txt;
txt.Format(_T("DICOM文件 数量: 0"));
GetDlgItem(IDC_STATIC_FILE)->SetWindowText(txt);
}
void CDcmImageDlg::Arrange()
{
CRect rc;
GetClientRect(&rc);
m_list.MoveWindow(10, 70, 300, rc.bottom - 80);
m_disp.MoveWindow(320, 10, rc.right - 320 - 110, rc.bottom - 20);
CRect rcBtn;
GetDlgItem(IDC_BUTTON_PREV)->GetWindowRect(&rcBtn);
int w = rcBtn.Width();
int h = rcBtn.Height();
int btnTop = 50;
GetDlgItem(IDC_BUTTON_PREV)->MoveWindow(rc.right - w - 10, btnTop, w, h);
GetDlgItem(IDC_BUTTON_NEXT)->MoveWindow(rc.right - w - 10, btnTop + h + 8, w, h);
}
void CDcmImageDlg::UpdateWidth(LPCTSTR lpszItem)
{
CFont *pFont = m_list.GetFont();
CClientDC dc(this);
dc.SelectObject(pFont);
CSize sz = dc.GetTextExtent(lpszItem, _tcslen(lpszItem));
sz.cx += (3 * ::GetSystemMetrics(SM_CXBORDER));
if (sz.cx > m_nListWidth)
{
m_nListWidth = sz.cx + 10;
m_list.SetHorizontalExtent(m_nListWidth);
}
}
void CDcmImageDlg::EnumFileInDir(CString dir, std::vector<CString>& vec, LPCTSTR ext /*= NULL*/)
{
CFileFind finder;
if (dir[dir.GetLength() - 1] != _T('\\')) dir += _T("\\");
if (finder.FindFile(dir + _T("*.*")))
{
BOOL bFind = FALSE;
do
{
bFind = finder.FindNextFile();
if (finder.IsDots())
continue;
else if (finder.IsDirectory())
{
CString strDir = finder.GetFilePath();
strDir += _T("\\");
EnumFileInDir(strDir, vec, ext);
}
else
{
CString fnext;
CString fn = finder.GetFileName();
int pos = fn.ReverseFind(_T('.'));
if (pos != -1)
{
CString strExt = ext;
fnext = fn.Mid(pos);
if (fnext.CompareNoCase(strExt) == 0)
vec.push_back(finder.GetFilePath());
}
}
} while (bFind);
}
}
void CDcmImageDlg::OnSelchangeListDcm()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
int idx = m_list.GetCurSel();
m_list.GetText(idx, str);
m_disp.LoadFile(str);
}
void CDcmImageDlg::OnBnClickedButtonPrev()
{
// TODO: 在此添加控件通知处理程序代码
int idx = m_list.GetCurSel();
int nCount = m_list.GetCount();
if (nCount == 0) return;
if (idx < 0) idx = 1;
if (idx == 0) {
return;
}
idx--;
m_list.SetCurSel(idx);
OnSelchangeListDcm();
}
void CDcmImageDlg::OnBnClickedButtonNext()
{
int idx = m_list.GetCurSel();
int nCount = m_list.GetCount();
if (nCount == 0) return;
if (idx < 0) idx = 0;
if (idx == nCount - 1) {
return;
}
idx++;
m_list.SetCurSel(idx);
OnSelchangeListDcm();
}
CString CDcmImageDlg::SelectFolder()
{
CString strFolder;
BROWSEINFO bi = { 0 };
bi.lpszTitle = _T("请选择文件夹"); // 对话框标题
// 设置初始目录(可选)
// TCHAR szInitDir[MAX_PATH] = _T("C:\\");
// bi.lpfn = BrowseCallbackProc; // 回调函数用于设置初始目录
// bi.lParam = (LPARAM)szInitDir;
// 显示文件夹选择对话框
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
if (pidl != NULL)
{
// 将选择的文件夹路径转换为字符串
TCHAR szPath[MAX_PATH];
if (SHGetPathFromIDList(pidl, szPath))
{
strFolder = szPath;
}
// 释放内存
CoTaskMemFree(pidl);
}
return strFolder;
}