将3D NumPy数组写入DICOM时的PixelData问题求助
我希望将灰度3D像素数组写入DICOM文件,但此前无该复杂文件格式的使用经验。使用pydicom库操作时出现疑似与PixelData属性相关的错误,不知如何修复。
最小复现代码
import os import math import numpy as np import datetime as dt from pydicom.dataset import FileDataset, FileMetaDataset import pydicom._storage_sopclass_uids dir_dcm = os.getcwd() im_stack = np.random.randint(0, (2**16)-1, size=(8406, 900, 900), dtype='uint16') max_val = np.max(im_stack, axis=(0, 1, 2)) bts_stored = int(math.log(max_val, 2)) max_val_hex = hex(max_val) min_val_hex = hex(0) dt = dt.datetime.now() file_meta = FileMetaDataset() file_meta.ImplementationClassUID = '1.2.3.4' file_meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid() file_meta.MediaStorageSOPClassUID = pydicom._storage_sopclass_uids.CTImageStorage file_meta.TransferSyntaxUID = pydicom.uid.ExplicitVRBigEndian pydicom.dataset.validate_file_meta(file_meta, enforce_standard=True) path_dcm = dir_dcm + '.dcm' mf_ds = FileDataset(path_dcm, {}, file_meta=file_meta) mf_ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.14.1' mf_ds.PatientName = "name^surname" mf_ds.PatientID = "00-000-000" mf_ds.ContentDate = dt.strftime('%Y%m%d') mf_ds.ContentTime = dt.strftime('%H%M%S.%f') mf_ds.is_little_endian = True mf_ds.is_implicit_VR = False mf_ds.SpecificCharacterSet = 'ISO_IR 100' mf_ds.SpacingBetweenSlices = r"0.5" mf_ds.PixelSpacing = r"0.5\\0.5" mf_ds.ImageOrientationPatient = r"1\\0\\0\\0\\1\\0" mf_ds.PhotometricInterpretation = "MONOCHROME2" mf_ds.PixelRepresentation = 0 mf_ds.PlanarConfiguration = 0 mf_ds.SmallestImagePixelValue = str.encode(min_val_hex) # zero mf_ds.LargestImagePixelValue = str.encode(max_val_hex) mf_ds.TotalPixelMatrixRows = im_stack.shape[2] mf_ds.TotalPixelMatrixColumns = im_stack.shape[1] mf_ds.Rows = im_stack.shape[2] mf_ds.Columns = im_stack.shape[1] mf_ds.NumberOfFrames = im_stack.shape[0] mf_ds.SamplesPerPixel = 1 mf_ds.BitsAllocated = 16 mf_ds.BitsStored = bts_stored mf_ds.HighBit = bts_stored-1 mf_ds.PixelRepresentation = 0 mf_ds.PixelData = im_stack.tobytes() mf_ds.save_as(dir_dcm+'.dcm', write_like_original=False)
错误信息
File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\spyder_kernels\py3compat.py", line 356, in compat_exec
exec(code, globals, locals)File "c:\users\kahy\skogforsk\ct-tör - general\kod\untitled0.py", line 65, in
mf_ds.save_as(dir_dcm+'.dcm', write_like_original=False)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\dataset.py", line 2061, in save_as
pydicom.dcmwrite(filename, self, write_like_original)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 1153, in dcmwrite
_write_dataset(fp, dataset, write_like_original)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 889, in _write_dataset
write_dataset(fp, get_item(dataset, slice(0x00010000, None)))File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 645, in write_dataset
dataset = correct_ambiguous_vr(dataset, fp.is_little_endian)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 242, in correct_ambiguous_vr
correct_ambiguous_vr_element(elem, ds, is_little_endian)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 198, in correct_ambiguous_vr_element
_correct_ambiguous_vr_element(elem, ds, is_little_endian)File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\filewriter.py", line 119, in _correct_ambiguous_vr_element
elem.value = convert_numbers(File "C:\Program Files\Miniconda3\envs\ct_env2\lib\site-packages\pydicom\values.py", line 389, in convert_numbers
raise BytesLengthException(BytesLengthException: Expected total bytes to be an even multiple of bytes per value. Instead received b'0x0' with length 3 and struct format 'H' which corresponds to bytes per value of 2.
错误原因
错误并非来自PixelData,而是SmallestImagePixelValue和LargestImagePixelValue字段。你用hex()把数值转成十六进制字符串(比如0x0)再编码成字节,但DICOM要求这两个字段是16位无符号整数,不是字符串格式。pydicom解析时把b'0x0'当成要转成16位整数的字节流,但其长度为3,不符合2字节的要求,因此报错。
另外还有几个潜在问题:
BitsStored计算错误:math.log(max_val,2)得到浮点数,转整数会丢失精度,比如max_val=65535时,计算结果≈15.9999,转int后为15,实际应为16。SOPClassUID与MediaStorageSOPClassUID不匹配:前者用了增强CT存储UID,后者用了普通CT存储UID,需要统一。- 部分属性赋值冗余:
SpacingBetweenSlices、PixelSpacing等字段不需要手动加双反斜杠,pydicom会自动处理格式。
修正后的代码
import os import numpy as np import datetime as dt from pydicom.dataset import FileDataset, FileMetaDataset import pydicom._storage_sopclass_uids dir_dcm = os.getcwd() # 生成测试用3D像素数组 im_stack = np.random.randint(0, (2**16)-1, size=(8406, 900, 900), dtype='uint16') max_val = np.max(im_stack) min_val = np.min(im_stack) # 正确计算BitsStored:取最高有效位长度 bts_stored = max_val.bit_length() if max_val !=0 else 1 current_dt = dt.datetime.now() # 构建文件元数据 file_meta = FileMetaDataset() file_meta.ImplementationClassUID = '1.2.3.4' file_meta.MediaStorageSOPInstanceUID = pydicom.uid.generate_uid() # 统一使用支持多帧的增强CT存储UID enhanced_ct_uid = pydicom._storage_sopclass_uids.EnhancedCTImageStorage file_meta.MediaStorageSOPClassUID = enhanced_ct_uid file_meta.TransferSyntaxUID = pydicom.uid.ExplicitVRBigEndian pydicom.dataset.validate_file_meta(file_meta, enforce_standard=True) path_dcm = os.path.join(dir_dcm, "output.dcm") mf_ds = FileDataset(path_dcm, {}, file_meta=file_meta, preamble=b"\x00"*128) mf_ds.SOPClassUID = enhanced_ct_uid mf_ds.PatientName = "name^surname" mf_ds.PatientID = "00-000-000" mf_ds.ContentDate = current_dt.strftime('%Y%m%d') mf_ds.ContentTime = current_dt.strftime('%H%M%S.%f')[:12] # DICOM时间最多12位 mf_ds.is_little_endian = True mf_ds.is_implicit_VR = False mf_ds.SpecificCharacterSet = 'ISO_IR 100' # 直接用数值或列表赋值,无需手动加反斜杠 mf_ds.SpacingBetweenSlices = 0.5 mf_ds.PixelSpacing = [0.5, 0.5] mf_ds.ImageOrientationPatient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0] mf_ds.PhotometricInterpretation = "MONOCHROME2" mf_ds.PixelRepresentation = 0 # 无符号整数 mf_ds.PlanarConfiguration = 0 # 直接赋值整数,符合DICOM的US类型要求 mf_ds.SmallestImagePixelValue = min_val mf_ds.LargestImagePixelValue = max_val # 多帧图像维度信息 mf_ds.TotalPixelMatrixRows = im_stack.shape[2] mf_ds.TotalPixelMatrixColumns = im_stack.shape[1] mf_ds.Rows = im_stack.shape[2] mf_ds.Columns = im_stack.shape[1] mf_ds.NumberOfFrames = im_stack.shape[0] mf_ds.SamplesPerPixel = 1 mf_ds.BitsAllocated = 16 mf_ds.BitsStored = bts_stored mf_ds.HighBit = bts_stored - 1 mf_ds.PixelRepresentation = 0 # 直接传入数组字节流 mf_ds.PixelData = im_stack.tobytes() mf_ds.save_as(path_dcm, write_like_original=False)
关键修正点说明
- 像素最值字段修正:将十六进制编码字符串改为直接赋值整数,符合DICOM对这两个字段的VR(US,无符号16位整数)定义。
- BitsStored计算优化:用
max_val.bit_length()替代math.log,确保得到准确的存储位数,避免精度丢失。 - SOP类UID统一:使用
EnhancedCTImageStorage,适配多帧3D图像的存储需求,与NumberOfFrames字段匹配。 - 属性赋值简化:
PixelSpacing、ImageOrientationPatient直接用列表赋值,pydicom会自动转换为DICOM要求的字符串格式。 - 时间格式修正:截取
ContentTime前12位,符合DICOM对时间字段的长度限制。
内容的提问来源于stack exchange,提问作者user8188435

