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

一、程序中的类介绍
└── 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:
#pragma onceclass 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
#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);elsepDataset->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);elsepDataset->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);elsepDataset->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);elsepDataset->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);elsedi->setPolarity(EPP_Reverse);}else{if (bReverse)di->setPolarity(EPP_Reverse);elsedi->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: // jpgcase 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: // bmpresult = 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);elsedi->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
#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 IDCString patName; // (0010,0010) PN Patient's NameCString sex; // (0010,0040) CS Patient's SexCString birthday; // (0010,0030) DA Patient's Birth DateCString studyAge; // (0010,1010) AS Patient's Agefloat patWeight; // (0010,1030) DS Patient's Weight
};struct StudyInfo
{CString studyInsUid; // (0020,000D) UI Study Instance UIDCString studyDesc; // (0008,1030) LO Study DescriptionCString studyId; // (0020,0010) SH Study IDCString accessionNumber; // (0008,0050) SH Accession NumberCString studyDate; // (0008,0020) DA Study DateCString studyTime; // (0008,0030) TM Study TimeCString modality; // (0008,0060) CS ModalityCString manufacturer; // (0008,0070) LO ManufacturerCString hospitalName; // (0008,0080) LO Institution NameCString hospitalAddr; // (0008,0081) ST Institution AddressCString operatorName; // (0008,1070) PN Operators' Name};struct SeriesInfo
{CString seriesInsUid; // (0020,000E) UI Series Instance UIDCString seriesDesc; // (0008,103E) LO Series DescriptionCString seriesDate; // (0008,0021) DA Series DateCString seriesTime; // (0008,0031) TM Series Timeint seriesNumber; // (0020,0011) IS Series Number// CTCString kvp; // (0018,0060) DS KVPCString exposureTime; // (0018,1150) IS Exposure TimeCString xRayTubeCurrent; // (0018,1151) IS X-Ray Tube CurrentCString exposure; // (0018,1152) IS Exposure// MRCString percentFOV; // (0018,0094) DS Percent Phase Field of ViewCString 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 Matrixint amiY; // (0018,1310) US 1 Acquisition Matrixint amjX; // (0018,1310) US 2 Acquisition Matrixint amjY; // (0018,1310) US 3 Acquisition MatrixCString protocolName; // (0018,1030) LO Protocol NameCString 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 OrientationCString poY; // (0020,0020) DS 1 Patient OrientationCString patPosition; // (0018,5100) CS Patient PositionCString sliceThickness; // (0018,0050) DS Slice ThicknessCString sliceLocation; // (0020,1041) DS Slice Locationint samplePerPixel; // (0028,0002) US Samples per Pixelfloat psX; // (0028,0030) DS 0 Pixel Spacingfloat psY; // (0028,0030) DS 1 Pixel Spacingint bitsAlloc; // (0028,0100) US Bits Allocatedint bitsStored; // (0028,0101) US Bits Storedint hightBit; // (0028,0102) US High Bitint pixelRep; // (0028,0103) US Pixel Representationfloat defWinCenter; // (0028,1050) DS Window Centerfloat defWinWidth; // (0028,1051) DS Window Widthfloat rescaleIntercept; // (0028,1052) DS Rescale Interceptfloat rescaleSlope; // (0028,1053) DS Rescale Slopeint height; // (0028,0010) US Rowsint width; // (0028,0011) US Columnsint instanceNumber; // (0020,0013) IS Instance NumberCString sopInsUid; // (0008,0018) UI SOP Instance UIDCString xfer; // (0002,0010) UI Transfer Syntax UIDCString imageType; // (0008,0008) CS Image TypeCString charset; // (0008,0005) CS Specific Character SetCString contentDate; // (0008,0023) DA Content DateCString contentTime; // (0008,0033) TM Content TimeCString acquisitionDate; // (0008,0022) DA Acquisition DateCString 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
#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
#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
#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;// Patientm_patInfo.patId = m_parser.GetTagValue(0x0010, 0x0020);strVal = m_parser.GetTagStringValue(0x0010, 0x0010);if (needTransToGbk)m_patInfo.patName = UTF8toA(strVal).c_str();elsem_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);}elsem_patInfo.birthday = tmp;m_patInfo.studyAge = m_parser.GetTagValue(0x0010, 0x1010);m_patInfo.patWeight = m_parser.GetTagFloatValue(0x0010, 0x1030);// Studym_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();elsem_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);}elsem_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);}elsem_studyInfo.studyTime = tmp;strVal = m_parser.GetTagStringValue(0x0008, 0x0070);if (needTransToGbk)m_studyInfo.manufacturer = UTF8toA(strVal).c_str();elsem_studyInfo.manufacturer = strVal.c_str();strVal = m_parser.GetTagStringValue(0x0008, 0x0080);if (needTransToGbk)m_studyInfo.hospitalName = UTF8toA(strVal).c_str();elsem_studyInfo.hospitalName = strVal.c_str();strVal = m_parser.GetTagStringValue(0x0008, 0x0081);if (needTransToGbk)m_studyInfo.hospitalAddr = UTF8toA(strVal).c_str();elsem_studyInfo.hospitalAddr = strVal.c_str();strVal = m_parser.GetTagStringValue(0x0008, 0x1070);if (needTransToGbk)m_studyInfo.operatorName = UTF8toA(strVal).c_str();elsem_studyInfo.operatorName = strVal.c_str();// Seriesm_seriesInfo.seriesInsUid = m_parser.GetTagValue(0x0020, 0x000E);strVal = m_parser.GetTagStringValue(0x0008, 0x103E);if (needTransToGbk)m_seriesInfo.seriesDesc = UTF8toA(strVal).c_str();elsem_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);}elsem_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);}elsem_seriesInfo.seriesTime = tmp;m_seriesInfo.seriesNumber = m_parser.GetTagIntValue(0x0020, 0x0011);// CTm_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);// MRm_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);// Imagem_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);}elsem_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);}elsem_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);}elsem_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);}elsem_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
#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 tiffbool 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
// Displayer.cpp: 实现文件
//#include "pch.h"
#include "DcmImage.h"
#include "Displayer.h"
#include "CDicomImage.h"
#include "Utilities.h"// DisplayerIMPLEMENT_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 兼容的内存 DCCBitmap bmp;bmp.CreateCompatibleBitmap(&dc, nWidth, nHeight); // 创建与客户区等大的位图CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 将位图选入内存 DCmemDC.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
#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
// DcmImageDlg.h: 头文件
//#pragma once#include "Displayer.h"// CDcmImageDlg 对话框
class CDcmImageDlg : public CDialogEx
{
// 构造
public:CDcmImageDlg(CWnd* pParent = nullptr); // 标准构造函数// 对话框数据
#ifdef AFX_DESIGN_TIMEenum { IDD = IDD_DCMIMAGE_DIALOG };
#endifprotected: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
// 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_TIMEenum { IDD = IDD_ABOUTBOX };
#endifprotected: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;
}
