解析医疗影像中的dicom文件
一、DICOM文件概述
我们先了解一下DICOM文件是什么,干嘛用的,以及DICOM内部有哪些信息,然后再谈如何去解析这些信息并转换成java对象。
医学影像学概览 医学影像学
这一学科致力于利用X射线、电磁场、超声波等多种介质与人体相互作用原理,将人体内部结构和密度以图像形式生动展现出来。这些可视化的信息为诊断医师提供了关键的决策依据,进而对个体健康状况进行专业评估。医学影像学的研究内容广泛,既包括构建及优化医学成像系统的硬件技术,也涵盖医学图像处理的软件算法。
DICOM标准解读 DICOM(Digital Imaging and Communications in Medicine)
作为国际公认的医学数字成像与通信标准(ISO 12052),在放射医学、心血管造影及多种放射诊疗设备(如X光、CT、PET、超声、MRI等)领域中广泛应用,并逐步渗透到眼科、牙科等其他医疗分支。DICOM格式文件是存储患者受保护健康信息(PHI)的重要载体,涵盖了姓名、性别、年龄等个人数据,以及图像采集设备信息、医疗背景资料等重要参数。目前采用的标准版本为DICOM3.0,每一张图像都蕴含丰富信息,主要分为四个类别:患者信息、检查研究信息、序列信息和图像信息,每一个信息单元通过独特的双字节十六进制标签(Group, Element)标识,如(0010,0010)即代表患者的姓名。
为了方便开发人员高效地处理DICOM数据,业界涌现了一批优秀的第三方库,如基于C++的DCMTK、基于Java的dcm4che以及Python语言环境下的pydicom。它们为开发者屏蔽了底层解析复杂性,极大地提高了项目开发效率。
当前,CT、MRI、超声等先进成像技术通过精准聚焦的射线束、探测器围绕人体特定区域进行连续断层扫描,从而生成多层面图像,经由三维重建技术可叠加成三维图像,而每一层面图像及其相关头部信息均可储存在DICOM文件中。DICOM文件的结构严谨,通常由文件头和数据集合两部分构成,其中文件头包含了识别文件属性的关键信息,且每个DICOM文件必备一个文件头。
深入剖析DICOM内部信息(DICOM Tag与VR)
DICOM数据集构成了DICOM文件的核心部分,其由一系列按特定顺序排列的DICOM数据元素组成。每个数据元素最小单位称为数据元,根据TAG值从小至大排序。数据元由四部分构成:
-
TAG编号: 由4个字节表示,包含2字节组号和2字节元素号,例如"0010 0040"对应患者性别信息,不同组号分别指示设备通讯信息、特性参数、患者信息和图像信息参数等内容。
-
值表示(VR, Value Representation): 两个字节定义了该数据元的数据类型,如LO(长字符串)、IS(整型字符串)、DA(日期)等共27种不同类型。
-
值长度(Value Length): 记录该数据项的具体长度。
-
值域(Value): 实际存储的数据值。
数据元素按照信息类别可分为四大类:患者、检查研究、序列和图像 ,形成一种患者可进行多次检查,每次检查涉及多个部位系列,每个部位系列关联一张或多张影像图像的层级结构。
1.常见的TAG
(1) Patient Tag(患者)
Group | Element | Tag Description | 中文解释 | 数据类型(VR) |
---|---|---|---|---|
10 | 10 | Patient’s Name | 患者姓名 | PN |
10 | 20 | Patient ID | 患者ID | LO |
10 | 30 | Patient’s Birth Date | 患者出生日期 | DA |
10 | 32 | Patient’s Birth Time | 患者出生时间 | TM |
10 | 40 | Patient’s Sex | 患者性别 | CS |
10 | 1030 | Patient’s Weight | 患者体重 | DS |
10 | 21C0 | Pregnancy Status | 怀孕状态 | US |
(2) Study Tag(患者)
Group | Element | Tag Description | 中文解释 | 数据类型(VR) |
8 | 50 | Accession Number:A RIS generated number that identifies order for the Study. | 检查号:RIS的生成序号,用以标识做检查的次序 | SH |
20 | 10 | Study ID | 检查ID | SH |
20 | 000D | Study Instance UID:Unique identifier for the Study. | 检查实例号:不同检查的唯一标识号 | UI |
8 | 20 | Study Date:Date the Study started. | 检查日期:检查开始的日期 | DA |
8 | 30 | Study Time:Time the Study started. | 检查时间:检查开始的时间 | TM |
8 | 61 | Modalities in Study | 一个检查中含有的不同检查类型 | CS |
8 | 15 | Body Part Examined | 检查的部位 | CS |
8 | 1030 | Study Description | 检查的描述 | LO |
10 | 1010 | Patient’s Age | 做检查时刻的患者年龄,而不是此刻患者的真实年龄 | AS |
(3) Series Tag(序列)
Group | Element | Tag Description | 中文解释 | 数据类型(VR) |
20 | 11 | Series Number:A number that identifies this Series. | 序列号:识别不同检查的号码 | IS |
20 | 000E | Series Instance UID:Unique identifier for the Series. | 序列实例号:不同序列的唯一标识号 | UI |
8 | 60 | Modality | 检查模态(MRI/CT/CR/DR) | CS |
8 | 103E | Series Description | 检查描述和说明 | LO |
8 | 21 | Series Date | 检查日期 | DA |
8 | 31 | Series Time | 检查时间 | TM |
20 | 32 | Image Position (Patient): The x,y and z coordinates of the upper left hand corner of the image,in mm. |
图像位置: 图像的左上角在空间坐标系中的x.y.z坐标,单位是毫米。如果在检查中,则指该序列中第一张影像左上角的坐标。 |
DS |
20 | 37 | Image Orientation (Patient):The direction cosines of the first row and the first column with respect to the patient. | 图像方位 | DS |
18 | 50 | Slice Thickness:Nominal slice thickness,in mm. | 层厚 | DS |
20 | 1041 | Slice Location:Relative position of exposure expressed in mm. | 实际的相对位置,单位为mm | DS |
18 | 23 | MR Acquisition | CS | |
18 | 15 | Body Part Examined | 身体部位 | CS |
(4) Image Tag(图像)
Group | Element | Tag Description | 中文解释 | 数据类型(VR) |
---|---|---|---|---|
8 | 8 | Image Type:Image identification characteristics. | CS | |
8 | 18 | SOP Instance UID | SOP实例UID | |
8 | 23 | Content Date: The date the image pixel data creation started. |
影像拍摄日期 | DA |
8 | 33 | Content Time | 影像拍摄时间 | TM |
20 | 13 | Image/Instance Number:A number that identifies this image. | 图像码:识别图像的号码 | IS |
28 | 2 | Samples Per Pixel:Number of samples (planes) in this image. | 图像采样率 | US |
28 | 4 | Photometric Interpretation: Specifies the intended interpretation of the pixel data. |
光度计解释: 对于CT图像,用两个枚举值MONOCHROME1,MONOCHROME2 用来判断图像是否是彩色的; MONOCHROME 1/2是灰度图,RGB则是真彩色图 |
CS |
28 | 10 | Rows : Number of rows in the image. | 图像的总行数,行分辨率 | US |
28 | 11 | Columns : Number of columns in the image. | 图像的总列数,列分辨率 | US |
28 | 30 | Pixel Spacing: Physical distance in the patient between the center of each pixel. |
像素间距: 像素中心之间的物理间距 |
DS |
28 | 100 | Bits Allocated: Number of bits allocated for each pixel sample.Each sample shall have the same number of bits allocated. |
分配的位数: 存储每一个像素值时分配的位数,每一个样本该值相同 |
US |
28 | 101 | Bits Stored: Number of bits stored for each pixel sample.Each sample shall have the same number of bits stored. |
存储的位数:有12到16列举值 存储每一个像素用的位数,每一个样本该值相同 |
US |
28 | 102 | High Bit: Most significant bit for pixel sample data. Each sample shall have the same high bit. |
高位 | US |
28 | 103 | Pixel Representation: Data representation of the pixel samples. Each sample shall have the same pixel representation. Enum:0000H=unsigned integer,0001H=2’ s complement. |
像素数据的表现类型: 一个枚举值,分别为十六进制数0000和0001. 0000H = 无符号整型, 0001H = 2的补码 |
US |
28 | 1050 | Window Center | 窗位 | DS |
28 | 1051 | Window Width | 窗宽 | DS |
28 | 1052 | Rescale Intercept: The value b in relationship between stored values(SV) and the output units. Output units = m*SV + b. Required if Modality LUT Sequence(0028, 0030) is not present. |
截距: 如果表明不同模态的LUT颜色对应表不存在时,则使用方程: Units = m*SV + b,计算真实的像素值到呈现像素值,其中截距为表达式中的b |
DS |
28 | 1053 | Rescale Slope: m in the equation specified by Rescale Intercept(0028, 1052). Required if Rescale Intercept is present. |
斜率: 该值为表达式中的m |
DS |
28 | 1054 | Rescale Type: Specifies the output units of Rescale Slope (0028,1053) and Rescale Intercept (0028,1052). Enum: US=Unspecified Requried if Photometric Interpretation is MONOCHROME2, and Bits Stored is greater than 1. This specifies an identity Modality LUT transformation. |
输出值的单位: 该值是一个枚举值 |
LO |
2.VR数据类型
VR是DICOM标准中用来描述数据类型的,总共有27个值。
27种数据类型
数据类型(VR) | 含义 | 允许字符 | 数据长度 |
---|---|---|---|
CS - Code String代码字符串 | 开头结尾可以有没有意义的空格的字符串,比如 “CD123_4" | 大写字母,0-9,空格以及下划线字符 | 最多16个字符 |
SH - Short String短字符串 | 短字符串,比如:电话号码, ID 等 | 最多16个字符 | |
LO - Long String长字符串 | 一个字符串,可能在开头、结尾填有空格。比如 “Introduction to DICOM” | 最多64个字符 | |
ST - Short Text短文本 | 可能包含一个或多个段落的字符串 | 最多1024个字符 | |
LT - Long Text长文本 | 可能包含一个或多个段落的字符串,与 LO 相同,但可以更长 | 最多10240个字符 | |
UT - Unlimited Text无限制文本 | 包含一个或多个段落的字符串,与 LT 类似 | 最多(232 -2)个字符 | |
AE - Application Entity应用实体 | 标识一个设备的名称的字符串,开头和结尾可以有无意义的字符。比如 “MyPCO 1” | 最多16个字符 | |
PN - Person Name病人姓名 | 有插入符号 (^) 作为姓名分隔符的病人姓名。比如“SMITH^JOHN” “Morrison Jones Susan^^^Ph.D,Chief Executive Officer” | 最多64个字符 | |
UI - Unique Identifier(UID)唯一标识符 | 一个用作唯一标识各类项目的包含UID的字符串。比如 “1.2.840.10008.1.1” | 0-9和半角句号 (.) | 最多64个字符 |
DA - Date日期 | 格式为 YYYYMMDD 的字符串;YYYY 代表年;MM 代表月;DD 代表日。比如 “20050822” 表示 2005 年 8 月 22 日 | 0-9 | 8个字符 |
TM - Time 时间 |
格式为 HHMMSS.FRAC 的字符串。 HH 表示小时(范围"00"-“23”); MM 表示分钟 (范围"00"-“59”); 而 FRAC 包含秒的小数部分,即百万分之一秒, 比如 “183200.00” 表示下午 6:32 |
0-9和半角句号 (.) | 最多16个字符 |
DT - Date Time 日期时间 |
格式为 YYYYMMDDHHMMSS.FFFFFF,串联的日期时间字符串。 字符串的各部分从左至右是:年-YYYY;月-MM;日-DD;小时-HH;分钟-MM;秒-SS;秒的小数-FFFFFF。 比如 “20050812183000.00” 表示 2005 年 8 月 12 日 下午 18 点 30 分 00 秒 |
0-9,加号,减号和半角句号 | 最多26个字符 |
AS - Age String 年龄字符串 |
符合以下格式的字符串:nnnD,nnnW,nnnM,nnnY;其中 nnn 对于 D 来说表示天数,对于 W 来说表示周数,对于 M 来说表示月数,对于 Y 来说表示岁数。比如 “018M” 表示他的年龄是 18 个月 | 0-9,D,W,M,Y | 4个字符 |
IS - Integer String整型字符串 | 表示一个整型数字的字符串,比如 “-1234567” | 0-9,加号 (+),减号 (-) | 最多12个字符 |
DS - Decimal String小数字符串 | 表示定点小数和浮点小数,比如 “12345.67”, “-5.0e3” | 0-9, 加号 (+), 减号 (-), 最多16个字符 E, e 和半角句号(.) | 最多16个字符 |
SS - Signed Short有符号短型 | 符号型二进制整数,长度 16 bits | 2个字符 | |
US - Unsigned Short无符号短型 | 无符号二进制整数,长度 16 bits | 2个字符 | |
SL - Signed Long有符号长型 | 有符号二进制整数 | 4个字符 | |
UL - Unsigned Long无符号长型 | 无符号二进制长整数,长度 32 bits | 4个字符 | |
AT - Attribute Tag属性标签 | 16 bits 无符号整数的有序对,数据元素的标签 | 4个字符 | |
FL - Floating Single单精度浮点型 | 单精度二进制浮点数 | 4个字符 | |
FD - Floating Point Double双精度二进制浮点型 | 双精度二进制浮点数 | 8个字符 | |
OB - Other Byte String其它字节字符串 | 字节的字符串("其它"表示没有在VR中定义的内容) | ||
OW - Other Word String其它单词字符串 | 16 bits(2字节) 单词字符串 | ||
OF - Other Float String其它浮点字符串 | 32 bits(4个字节) 浮点单词字符串 | ||
SQ - Sequence Items条目序列 | 条目的序列 | ||
UN - Unknown未知 | 字节的字符串,其中内容的编码方式是未知的 |
上面TAG数据均可通过dcm查看工具查看,也能修改:
二、java解析DICOM的TAG
导入dcm4che3到maven中
<dependency>
<groupId>org.dcm4che</groupId>
<artifactId>dcm4che-core</artifactId>
<version>3.3.8</version>
</dependency>
然后上代码:
package com.wanputech.film.util;
import com.alibaba.fastjson.JSON;
import com.wanputech.film.domain.dto.DicomInfo;
import org.apache.commons.lang.StringUtils;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.DicomInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* 解析获取dcm文件信息
*
* @author dengjiajin
* @since 2024/1/10
*/
public class RadiologyParserUtil {
public static void main(String[] args) throws IOException {
String dcmPath = "D:\\radiology1\\11151\\1609000006C7_0001_0001.dcm";
DicomInfo dicomInfo = getDicomInfo(dcmPath);
System.out.println(JSON.toJSONString(dicomInfo));
}
/**
* 解析获取dcm文件信息
* @param dcmPath
* @return
*/
public static DicomInfo getDicomInfo(String dcmPath) throws IOException {
File file = new File(dcmPath);
DicomInputStream dis = new DicomInputStream(file);
Attributes attrs = dis.readDataset(-1, -1);
dis.close();
String specificCharset = attrs.getString(Tag.SpecificCharacterSet);
if (specificCharset.equals("ISO_IR 192") || specificCharset.equals("GB18030")) {
// 设置系统属性以影响dcm4che库内部的字符集转换
System.setProperty("dcm4che.charset", "GB18030");
} else if (specificCharset.equals("ISO_IR 58") || specificCharset.equals("GBK")) {
System.setProperty("dcm4che.charset", "GBK");
}
String patientName = attrs.getString(Tag.PatientName);
String patientID = attrs.getString(Tag.PatientID);
String modality = attrs.getString(Tag.Modality);
String studyDate = attrs.getString(Tag.StudyDate);
String studyTime = attrs.getString(Tag.StudyTime);
String studyInstanceUid = attrs.getString(Tag.StudyInstanceUID);
String bodyPartExaminedStr = new String(attrs.getBytes(Tag.BodyPartExamined)
, Charset.forName(attrs.getString(Tag.SpecificCharacterSet)));
DicomInfo dicomInfo = new DicomInfo();
dicomInfo.setPatientName(patientName);
dicomInfo.setPatientId(patientID);
dicomInfo.setModality(modality);
dicomInfo.setStudyDateStr(studyDate);
dicomInfo.setStudyTimeStr(studyTime);
studyDateTimeStrConverseToDate(dicomInfo);
dicomInfo.setBodyPartExamined(bodyPartExaminedStr);
dicomInfo.setAge(parseAge(attrs.getString(Tag.PatientAge)));
dicomInfo.setSex(parseSex(attrs.getString(Tag.PatientSex)));
dicomInfo.setStudyInstanceUid(studyInstanceUid);
return dicomInfo;
}
private static String parseSex(String sex) {
if ("F".equals(sex)){
return "1";
}else {
return "0";
}
}
private static String parseAge(String n) {
Integer patientAge = null;
Pattern pattern = Pattern.compile("(\\d+)Y");
Matcher matcher = pattern.matcher(n);
if (matcher.find()) {
patientAge = Integer.valueOf(matcher.group(1));
}
Pattern pattern2 = Pattern.compile("(\\d+)M");
Matcher matcher2 = pattern2.matcher(n);
if (matcher2.find()) {
patientAge = Integer.valueOf(matcher2.group(1));
patientAge = patientAge / 12;
}
Pattern pattern3 = Pattern.compile("(\\d+)D");
Matcher matcher3 = pattern3.matcher(n);
if (matcher3.find()) {
patientAge = Integer.valueOf(matcher3.group(1));
patientAge = patientAge / 365;
}
return patientAge.toString();
}
private static void studyDateTimeStrConverseToDate(DicomInfo dicomInfo) {
String studyDateStr = dicomInfo.getStudyDateStr();
String studyTimeStr = dicomInfo.getStudyTimeStr();
if (studyDateStr != null && studyTimeStr != null)
String dateTimeStr = studyDateStr + " " + studyTimeStr.substring(0, 6); // 去掉微秒部分
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, formatter);
ZoneId zoneId = ZoneId.systemDefault(); // 或者指定时区 TimeZone.getDefault().toZoneId()
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
Date studyTime = Date.from(zonedDateTime.toInstant());
dicomInfo.setStudyTime(studyTime);
}
}
}
解析的代码主要是getDicomInfo这个方法。
这段Java代码主要功能是从DICOM文件中读取相关信息,并将其保存在一个自定义的DicomInfo对象中。
-
首先,通过File对象创建一个DicomInputStream,然后使用readDataset方法读取DICOM文件中的数据集,并将其保存在Attributes对象中。
-
根据DICOM文件中的SpecificCharacterSet属性值,设置系统属性dcm4che.charset,以影响dcm4che库内部的字符集转换。
-
从Attributes对象中获取并保存一些重要的DICOM属性值,如PatientName、PatientID、Modality、StudyDate、StudyTime、StudyInstanceUID和BodyPartExamined等。
-
创建一个DicomInfo对象,并将之前获取的DICOM属性值设置到该对象中。调用studyDateTimeStrConverseToDate方法对studyDateTimeStr进行日期时间转换,并将结果保存在DicomInfo对象中。
-
调用parseAge和parseSex方法分别解析PatientAge和PatientSex属性值,并将结果保存在DicomInfo对象中。
最终,这段代码将包含DICOM文件信息的DicomInfo对象返回或进行后续处理。
dcm返回的age格式是40Y,08M,24D,这种格式最后的字母分别代表Y(年),M(月),D(天),这里需要特殊处理。
领取dcm4che包、查看dcm文件工具、dcm文件模版。扫码添加我的公众号。
如果您有什么问题、或领取dcm4che包、查看dcm文件工具、dcm文件模版。扫码添加我的公众号留言或添加我的微信留言即可。届时也会分享一些日常工作遇到的难题。
公众号:
微信号:11141004