lshk0124
 

DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件”

分类: DICOM
 

目录(?)[+]

 

背景:

        本专栏“DICOM医学图像处理”受众较窄,起初只想作为自己学习积累和工作经验的简单整理。前几天无聊浏览了一下,发现阅读量两极化严重,主要集中在“关于BMP(JPG)与DCM格式转换”和“DICOM 通讯协议”,尤其是许久前的第一篇博文DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组)。因此在2014年底前打算写几篇关于DCM格式转换的文章,此次主要聚焦“如何将BMP、JPG等常规图像保存成DCM文件”,以DCMTK库为基础,给出简单的实例。

        这几篇博文采用倒叙的方式,先给出可直接运行的源码,然后重点讲解其中易犯的错误,最后是知识点补充。

利用DCMTK实现Multi-BMP存入DCM:

        源码以DCMTK为基础,思路参照DCMTK的img2dcm工具包,依赖库包含:netapi32.lib; wsock32.lib; ofstd.lib; oflog.lib; dcmimgle.lib; ijg8.lib; ijg12.lib; ijg16.lib; dcmdata.lib; dcmimage.lib; dcmjpeg.lib; dcmnet.lib; zlib.lib;libi2d.lib;(【注】:libi2d.lib库是用于导入BMP文件的)

源码如下:

 

[csharp] view plaincopyprint?
 
  1. // DcmPixelDataTest.cpp : 定义控制台应用程序的入口点。  
  2. //  
  3.  
  4. #include "stdafx.h"  
  5. #include "dcmtk/config/osconfig.h"  
  6. #include "dcmtk/dcmdata/dctk.h"  
  7. #include "dcmtk/dcmdata/dcistrmf.h"  
  8. #include "dcmtk/dcmdata/libi2d/i2dbmps.h"  
  9. #include "DicomUtils.h"  
  10. #include <direct.h>  
  11.   
  12. int _tmain(int argc, _TCHAR* argv[])  
  13. {  
  14.     OFCondition status;  
  15.   
  16.     DcmFileFormat fileformat;  
  17.     DcmDataset* mydatasete=fileformat.getDataset();  
  18.     DicomUtils::AddDicomElements((DcmDataset*&)mydatasete);  
  19.     Uint16 rows,cols,samplePerPixel,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV;  
  20.     OFString photoMetrInt;  
  21.     Uint32 length;  
  22.     E_TransferSyntax ts;  
  23.     char* mydata=new char[1024*1024*10];  
  24.     memset(mydata,0,sizeof(char)*1024*1024*10);  
  25.     char* tmpData=mydata;  
  26.     char curDir[100];  
  27.     getcwd(curDir,100);  
  28.     //循环添加4张图片  
  29.     for(int i=0;i<4;++i)  
  30.     {  
  31.         OFString num;  
  32.         char numtmp[100];  
  33.         memset(numtmp,0,sizeof(char)*100);  
  34.         sprintf(numtmp,"%s\\test\\%d.bmp",curDir,i+1);  
  35.         OFString filename=OFString(numtmp);  
  36.         I2DBmpSource* bmpSource=new I2DBmpSource();  
  37.         bmpSource->setImageFile(filename);  
  38.   
  39.         char* pixData=NULL;  
  40.         bmpSource->readPixelData(rows,cols,samplePerPixel,photoMetrInt,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV,pixData,length,ts);  
  41.         memcpy(tmpData,pixData,length);  
  42.         tmpData+=length;  
  43.   
  44.         delete bmpSource;  
  45.     };  
  46.   
  47.     mydatasete->putAndInsertUint16(DCM_SamplesPerPixel,samplePerPixel);  
  48.     mydatasete->putAndInsertString(DCM_NumberOfFrames,"4");  
  49.     mydatasete->putAndInsertUint16(DCM_Rows,rows);  
  50.     mydatasete->putAndInsertUint16(DCM_Columns,cols);  
  51.     mydatasete->putAndInsertUint16(DCM_BitsAllocated,bitsAlloc);  
  52.     mydatasete->putAndInsertUint16(DCM_BitsStored,bitsStored);  
  53.     mydatasete->putAndInsertUint16(DCM_HighBit,highBit);  
  54.     mydatasete->putAndInsertUint8Array(DCM_PixelData,(Uint8*)mydata,4*length);  
  55.     mydatasete->putAndInsertOFStringArray(DCM_PhotometricInterpretation,photoMetrInt);  
  56.     //mydatasete->putAndInsertString(DCM_PlanarConfiguration,"1");  
  57.     status=fileformat.saveFile("c:\\Multibmp2dcmtest.dcm",ts);  
  58.     if(status.bad())  
  59.     {  
  60.         std::cout<<"Error:("<<status.text()<<")\n";  
  61.     }  
  62.     return 0;  
  63. }  

 

代码中的DicomUtils类是一个方法类,提供了一个静态方法AddDicomElement构造DICOM基本元素,代码如下:

 

[csharp] view plaincopyprint?
 
  1. #include "DicomUtils.h"  
  2.   
  3.   
  4. DicomUtils::DicomUtils(void)  
  5. {  
  6. }  
  7.   
  8.   
  9. DicomUtils::~DicomUtils(void)  
  10. {  
  11. }  
  12. void DicomUtils::AddDicomElements(DcmDataset*& dataset)  
  13. {  
  14.     //构建测试数据  
  15.   
  16.     /*  添加患者信息  */  
  17.     dataset->putAndInsertUint16(DCM_AccessionNumber,0);  
  18.     dataset->putAndInsertString(DCM_PatientName,"zssure",true);  
  19.     dataset->putAndInsertString(DCM_PatientID,"2234");  
  20.     dataset->putAndInsertString(DCM_PatientBirthDate,"20141221");  
  21.     dataset->putAndInsertString(DCM_PatientSex,"M");  
  22.   
  23.     /*  添加Study信息   */  
  24.     dataset->putAndInsertString(DCM_StudyDate,"20141221");  
  25.     dataset->putAndInsertString(DCM_StudyTime,"195411");  
  26.     char uid[100];  
  27.     dcmGenerateUniqueIdentifier(uid,SITE_STUDY_UID_ROOT);  
  28.     dataset->putAndInsertString(DCM_StudyInstanceUID,uid);  
  29.     dataset->putAndInsertString(DCM_StudyID,"1111");  
  30.   
  31.   
  32.     /*  添加Series信息  */  
  33.     dataset->putAndInsertString(DCM_SeriesDate,"20141221");  
  34.     dataset->putAndInsertString(DCM_SeriesTime,"195411");  
  35.     memset(uid,0,sizeof(char)*100);  
  36.     dcmGenerateUniqueIdentifier(uid,SITE_SERIES_UID_ROOT);  
  37.     dataset->putAndInsertString(DCM_SeriesInstanceUID,uid);  
  38.     /*  添加Image信息   */  
  39.     dataset->putAndInsertString(DCM_ImageType,"ORIGINAL\\PRIMARY\\AXIAL");  
  40.     dataset->putAndInsertString(DCM_ContentDate,"20141221");  
  41.     dataset->putAndInsertString(DCM_ContentTime,"200700");  
  42.     dataset->putAndInsertString(DCM_InstanceNumber,"1");  
  43.     dataset->putAndInsertString(DCM_SamplesPerPixel,"1");  
  44.     dataset->putAndInsertString(DCM_PhotometricInterpretation,"MONOCHROME2");  
  45.     dataset->putAndInsertString(DCM_PixelSpacing,"0.3\\0.3");  
  46.     dataset->putAndInsertString(DCM_BitsAllocated,"16");  
  47.     dataset->putAndInsertString(DCM_BitsStored,"16");  
  48.     dataset->putAndInsertString(DCM_HighBit,"15");  
  49.     dataset->putAndInsertString(DCM_WindowCenter,"600");  
  50.     dataset->putAndInsertString(DCM_WindowWidth,"800");  
  51.     dataset->putAndInsertString(DCM_RescaleIntercept,"0");  
  52.     dataset->putAndInsertString(DCM_RescaleSlope,"1");  
  53.   
  54.   
  55. }  

 

问题分析:

1)文件格式错误:

        我在方法类DicomUtils中默认添加的SamplePerPixel标签值为1,如果最终读取完像素数据(即readPixelData函数调用完)未重新写入BMP相应的SamplePerPixel字段,会引发文件格式错误,在SanteSoft DICOM Editor软件中打开弹出如下错误:

image

        利用DCMTK自带的dcmdump.exe工具分析结果如下:

image

        由此可以看出像素数据读取失败。

2)图像信息显示错误:

 

        上图是正确图像,下图是由于Photometric Interpretation字段写入错误导致的,静态类DicomUtils中默认的Photometric Interpretation值为MONOCHROME2,修改为I2DBmpSource中readPixelData函数返回的photoMetrInt参数后图像数据显示正确,如下图所示:

 

3)图像色彩显示错误:

        在静态类DicomUtils中并未添加Planar Configuration字段,因此DCMTK自动填充该字段为0,如果我们修改为1,会出现图像色彩错误,如下图:

BMP格式:

        关于BMP格式介绍的博文很多,可参考http://blog.csdn.net/zhandoushi1982/article/details/5196017或者http://blog.csdn.net/gwwgle/article/details/4775396。BMP文件数据主要有以下几部分组成:1)文件头,即结构BITMAPFILEHEADER, *PBITMAPFILEHEADER,类似于DCM中的DcmMetaInfo;2)图像描述信息块,该部分记录了图像信息块的大小、图像的宽度、高度、图像通道数(即Plane)、像素位数(即后面DICOM标准中的SamplesPerPixel)、图像压缩方式、图像数据区大小等等;3)颜色表,即调色板。该部分与DICOM标准中的COLOR PALETTE,随着像素位数不同颜色表大小也不同,当像素位数为24或更大,即SamplesPerPixel=3时,像素数据本身就可以代表颜色,因此不需要颜色表;4)图像数据区,即文件中存储的真正的像素信息。【注】:这里有一个坐标转换,标准的BMP文件像素存储顺序是由左到右、由下到上,即坐标原点为图像左下角;而DICOM标准存储顺序为从左到右,从上到下,坐标原点为图像左上角,因此在自己读取时需要进行反转。

获取BMP图像信息方法:

1)直接读取二进制

        了解了BMP文件的具体格式,可以利用常用的二进制操作方式,直接从文件中提取像素数据。这种代码网上也很多,可参考:http://www.jb51.net/article/56274.htm

2)DCMTK库

        DCMTK库中的I2DBmpSource类是专门用来解析BMP文件的,并且提供了BMP到DICOM数据格式的转换。具体的使用可参照我上面的实例,也可参考DCMTK给出的img2dcm工具包源码。 

3)CxImage第三方库

        CxImage是一款免费的、优秀的图像操作类库,可以快捷的存取、显示、转换各种图像,例如BMP、GIF、ICO、TGA、JPEG、PCX、PNG、TIFF、MNG、RAS等等;CxImage使用简单,文档详细,只有一个API接口文件;支持Windows、Linux和Unix等多平台,支持32位和64位。

        关于CxImage的复杂使用,可参见CodeProject中大神的博文:http://www.codeproject.com/Articles/1300/CxImage

        另外我在博文DCMTK开源库的学习笔记1:将DCM文件保存成BMP文件或数据流(即数组)中给出的源码是结合了CxImage和DCMTK两种开源库,这也是常见的一种组合方式,具体细节可参考我的GitHub上的源码。

DICOM文件格式:

1)Samples Per Pixel:

        标签为(0028,0002),具体的介绍在DICOM3.0标准第3部分的附录C7.6.3.1。含义表示【the number of separate planes in this image】,就像PhotoShop中的通道,每个通道表示一种颜色(除了RGB三个通道以外,也会存在第四个通透性通道)。对于灰度图像(monochrome或gray)和颜色表图像(palette,就是BMP格式中介绍的有调色板的BMP文件),该标签值为1,RGB图像或其他色彩模式图像,该标签值为3。本实例中使用的BMP图像是RGB格式的,因此SamplePerPixel=3,起初的文件格式错误就是由于该字段设置为1所致。

2)Photometric Interpretation:

        标签为(0028,0004),具体介绍在DICOM3.0标准第3部分的附录C7.6.3.1.2。该字段常见的值有MONOCHROME1、MONOCHROME2、PALETTE COLOR、RGB,其中MONOCHROME1和MONOCHROME2表示单通道灰度图像,只是两者对黑色和白色的映射相反而已;PALETTE COLOR就是BMP中提到的调色板图像,此时需要SamplesPerPixel字段为1,;RGB是常见的R(红)、G(绿)、B(蓝)三通道彩色图像,此时SamplesPerPixel字段值为3,这就是我们实例中使用的图像。除此以外DICOM3.0标准中还给出了YBR_FULL、HSV、ARGB、CMYK等方式,此处就不详细介绍了。

3)Planar Configuration:

        标签为(0028,0006),具体介绍在DICOM3.0标准第3部分附录C7.6.3.1.3。当Samples Per Pixel字段的值大于1时,Planar Configuration字段规定了实际像素信息的存储方式,具体如下:

image

 

最终结果:

        博文中实例代码最终在C盘根目录生成Multibmp2dcmtest.dcm文件,利用Sante DICOM Editor打开可以顺利看到文件中包含了我们插入的四张bmp图像,如下图:

 

        至此将多幅BMP图像写入DCM文件的任务顺利完成了,其实将多幅图像写入DCM文件与写入单幅BMP图像是完全相同的,只需要将多张BMP图像(此时要求每张BMP图像的宽度和高度相同)像素数据首尾相接的写入DCM中的PixelData标签下,即(7FE0,0010);此时将NumberofFrames标签赋值为图像张数,DCM文件编辑器就可自动识别提取各张图像。

源码:

百度网盘:http://pan.baidu.com/s/1dDrhHlR

GitHub:https://github.com/zssure-thu/CSDN/tree/master

网址为:http://blog.csdn.net/zssureqh/article/details/42119303

背景:

        续上篇,继续介绍如何将多幅JPG图像数据存入DCM文件。即将有损压缩数据直接写入DCM文件,存储为Multi-frame形式。

多幅JPG图像数据存入DCM文件:

        为了避免引起歧义,这里着重说明一下。本博文的描述的场景是:假设我们手中有多张JPG文件,想把JPG文件写入DCM文件,即单个DCM文件包含多幅图像信息的Multi-Frame形式。该问题之前与CSDN博友y317215133y也讨论过,当时我在OFFIS论坛中找到了一个帖子直接给了y317215133y答复。今天重新梳理了一下发现,当时帖子中的情况与我今天要描述的问题略有不同:帖子中作者已经拥有多张图像的原始数据(从作者的描述来看,该数据是非压缩的),希望将该系列数据以压缩形式写入DCM文件中。想必作者执行该操作的目的是减少存储空间,而本博文中我拥有的是JPEG压缩的数据,也就是说我不是为了减少存储空间,而单纯的就是希望将多幅JPEG格式的图像存成Multi-frame DCM格式,便于归档管理。

        帖子中OFFIS DICOM Team人员给出的答复是:1)创建DcmFileFormat对象,利用getDataset()获得其中的数据体指针;2)利用putAndInsertXXX向1)中的Dataset写入非压缩的原始图像数据,即上一篇博文DICOM医学图像处理:DICOM存储操作之“多幅BMP图像数据存入DCM文件”所采用的方法;3)注册JPEG编码参数,例如DJ_PRLossless、DJ_RPLossy等,然后调用chooseRepresentation函数。该部分操作就是对DCM文件进行JPEG有损或无损压缩,具体过程可参照dcmcjpeg.cc中的代码;4)调用saveFile函数将编码后的数据写入Multi-fram DCM文件。

        以上四步操作并未使用DcmPixelSequence类,帖子作者以及博友y317215133y在这种场景下却希望使用DcmPixelSequence学习一下SQ字段的写入操作,其实是选择场景错误才导致错误使用DcmPixelSequence类。帖子最后作者也给出了提示,如下图:

        上述正是本文要做的事情,希望通过该实例来讲解DcmPixelSequence类的使用,并进一步学习JPEG压缩的Multi-frame DCM文件。

代码实例:

        参照OFFIS论坛中的代码http://forum.dcmtk.org/viewtopic.php?t=1544&highlight=creating+multiframe+dicom+images,直接给出源码:

 

[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. // DcmPixelDataTest.cpp : 定义控制台应用程序的入口点。  
  2. //  
  3.  
  4. #include "stdafx.h"  
  5. #include "dcmtk/config/osconfig.h"  
  6. #include "dcmtk/dcmdata/dctk.h"  
  7. #include "dcmtk/dcmdata/dcistrmf.h"  
  8. #include "dcmtk/dcmdata/dcpixel.h"  
  9. #include "dcmtk/dcmdata/dcpixseq.h"  
  10. #include "dcmtk/dcmdata/dcpxitem.h"  
  11. /*----BMP图像解析----*/  
  12. #include "dcmtk/dcmdata/libi2d/i2dbmps.h"  
  13. #include "DicomUtils.h"  
  14. /*----JPEG图像解析----*/  
  15. #include "dcmtk/dcmdata/libi2d/i2djpgs.h"  
  16. #include "dcmtk/dcmdata/libi2d/i2doutpl.h"  
  17. #include "dcmtk/dcmdata/dcerror.h"  
  18.  
  19. #include <direct.h>  
  20.   
  21.   
  22.   
  23. int _tmain(int argc, _TCHAR* argv[])  
  24. {  
  25.     OFCondition status;  
  26.   
  27.     DcmFileFormat fileformat;  
  28.     DcmDataset* mydatasete=fileformat.getDataset();  
  29.     DicomUtils::AddDicomElements((DcmDataset*&)mydatasete);  
  30.     Uint16 rows,cols,samplePerPixel,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV;  
  31.     OFString photoMetrInt;  
  32.     Uint32 length;  
  33.     E_TransferSyntax ts;  
  34.     char curDir[255];  
  35.     getcwd(curDir,255);  
  36.     DcmPixelSequence *seq=new DcmPixelSequence(DcmTag(DCM_PixelData,EVR_OB));  
  37.     /*!------zssure:begin,添加一个空的Dicom Pixel Item充当Offset Fragment------!*/  
  38.     //seq->insert(new DcmPixelItem(DcmTag(DCM_Item,EVR_OB)));  
  39.     /*-------zssure:end,可顺利解决多幅JPEG存入DCM的问题-------------------------*/  
  40.     //循环添加4张图片  
  41.     for(int i=0;i<4;++i)  
  42.     {  
  43.         OFString num;  
  44.         char numtmp[255];  
  45.         memset(numtmp,0,sizeof(char)*255);  
  46.         sprintf(numtmp,"%s\\jpeg-test\\%d.jpg",curDir,i+1);  
  47.         OFString filename=OFString(numtmp);  
  48.         I2DJpegSource* bmpSource=new I2DJpegSource();  
  49.         bmpSource->setImageFile(filename);  
  50.   
  51.         char* pixData=NULL;  
  52.         bmpSource->readPixelData(rows,cols,samplePerPixel,photoMetrInt,bitsAlloc,bitsStored,highBit,pixelRpr,planConf,pixAspectH,pixAspectV,pixData,length,ts);  
  53.   
  54.         DcmPixelItem *newItem=new DcmPixelItem(DcmTag(DCM_Item,EVR_OB));  
  55.         if(newItem!=NULL)  
  56.         {  
  57.             seq->insert(newItem);  
  58.             OFCondition result=newItem->putUint8Array((Uint8*)pixData,length);  
  59.   
  60.         }  
  61.         delete bmpSource;  
  62.     };  
  63.   
  64.     mydatasete->putAndInsertUint16(DCM_SamplesPerPixel,samplePerPixel);  
  65.     mydatasete->putAndInsertString(DCM_NumberOfFrames,"4");  
  66.     mydatasete->putAndInsertUint16(DCM_Rows,rows);  
  67.     mydatasete->putAndInsertUint16(DCM_Columns,cols);  
  68.     mydatasete->putAndInsertUint16(DCM_BitsAllocated,bitsAlloc);  
  69.     mydatasete->putAndInsertUint16(DCM_BitsStored,bitsStored);  
  70.     mydatasete->putAndInsertUint16(DCM_HighBit,highBit);  
  71.     mydatasete->putAndInsertOFStringArray(DCM_PhotometricInterpretation,photoMetrInt);  
  72.     mydatasete->insert(seq,OFFalse,OFFalse);  
  73.     status=fileformat.saveFile("c:\\MultiJpeg2Multi-frameDCMtest-error.dcm",ts);  
  74.     if(status.bad())  
  75.     {  
  76.         std::cout<<"Error:("<<status.text()<<")\n";  
  77.     }  
  78.     return 0;  
  79. }  

PS:DicomUtils类是DICOM文件操作静态类,具体见后续工程源码。

 

       上述代码可以顺利生成Multi-frame DCM文件,从文件大小来看结果也应该正常。但是打开时却提示“内存无法读取错误”,如下图:

 

 

        但是比较奇怪的是,利用dcmdump.exe工具和Sante DICOM Editor的预览窗口(Enable Icons)却可以看到正常的结果。如下图所示:

错误分析:

DICOM标准中的JPEG压缩

        DICOM3.0标准第5部分附录A中给出了协议中常见的JPEG压缩格式,如下图:

 

 

 

 

        常见的JPEG图像采用的就是1.2.840.10008.1.2.4.50,本博文中给出的四副测试图像就是这种格式。至于JPEG具体的压缩和编码流程可参考wiki百科http://zh.wikipedia.org/zh-cn/JPEG

        标准中指出,如果DICOM文件时Multi-frame类型,每幅图像(frame)需要分别压缩(encoded seperately)。压缩数据在写入DICOM中的DcmPixelData字段时可能会被分片(fragment),切记:每个片段(fragment)中的数据一定来自同一文件(即frame),而每幅图像(frame)不一定存储在同一个片段(fragment),因此frame与fragment之间的对应关系是“一对多”

DcmPixelData字段(7FE0,0010):

        DICOM3.0标准第5部分第8章指出,如果数据以压缩形式存储,那么PixelData的VR只能采用OB(原始数据存储通常采用OW,如果数据存储位数小于等于8也可以采用OB形式,正如我的上一篇博文。压缩数据会被分割为包含自身长度的多个片段(fragments),最终以截止符(FFFE,E0DD)结束。如下图所示:

 

 

 

 

        一幅图像(frame)可以包含在一个片段(fragment)中,也可以被分割为多个片段。使用时可通过比较【字段NumberOfFrames(0028,0008)】与【字段PixelData的Item个数-1】来判别,

NumberOfFrames==ItemsOfPixelData-1,表明每幅图像都包含在一个片段里(fragment);

NumberOfFrames<ItemsOfPixelData-1,表明有图像被分为多个片段存储;

解决方案:

        注意,上面需要对PixelData字段的Items数【减去1】,如表A.4-1、A.4-2所示,无论如何PixelData字段中都会包含一个Offset item。——这正是我们上述代码错误的原因,为了证明这一点,让我们在插入各幅图像之前添加一个空的DcmPixelItem,即在for循环之前添加如下两行代码:

 

[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. /*!------zssure:begin,添加一个空的Dicom Pixel Item充当Offset Fragment------!*/  
  2. seq->insert(new DcmPixelItem(DcmTag(DCM_Item,EVR_OB)));  
  3. /*-------zssure:end,可顺利解决多幅JPEG存入DCM的问题-------------------------*/  

 

 

        此刻用DICOM浏览器可以顺利打开我们生成的MultiJPEG2DCMtest.dcm,如下图所示:

 

        利用二进制查看器可以看到,PixelData字段多了一个SQ Item,即充当Offset的空的DcmPixelItem,如下图所示:

 

        另外从最终文件大小可以看出,这种方式并未减少存储空间。

 

备注:

        DICOM3.0标准中给出了DCM数据的压缩方法和存储方式,至于压缩数据(有损压缩,例如本例中采用的1.2.840.10008.1.2.4.50)在临床是否有应用价值不属于协议考虑范围。

 

 原网址为:http://blog.csdn.net/zssureqh/article/details/42200303

分类:

技术点:

相关文章:

  • 2021-09-10
  • 2022-02-11
  • 2022-03-04
  • 2021-06-04
  • 2022-01-06
  • 2021-12-26
猜你喜欢
  • 2021-12-26
  • 2021-10-19
  • 2021-12-06
  • 2021-12-03
相关资源
相似解决方案