dicube 0.1.4__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. dicube/__init__.py +140 -0
  2. dicube/codecs/__init__.py +152 -0
  3. dicube/codecs/jph/__init__.py +15 -0
  4. dicube/codecs/jph/codec.py +161 -0
  5. dicube/codecs/jph/ojph_complete.cp310-win32.pyd +0 -0
  6. dicube/codecs/jph/ojph_complete.cp310-win_amd64.pyd +0 -0
  7. dicube/codecs/jph/ojph_complete.cp311-win32.pyd +0 -0
  8. dicube/codecs/jph/ojph_complete.cp311-win_amd64.pyd +0 -0
  9. dicube/codecs/jph/ojph_complete.cp38-win32.pyd +0 -0
  10. dicube/codecs/jph/ojph_complete.cp38-win_amd64.pyd +0 -0
  11. dicube/codecs/jph/ojph_complete.cp39-win32.pyd +0 -0
  12. dicube/codecs/jph/ojph_complete.cp39-win_amd64.pyd +0 -0
  13. dicube/codecs/jph/ojph_decode_complete.cp310-win32.pyd +0 -0
  14. dicube/codecs/jph/ojph_decode_complete.cp310-win_amd64.pyd +0 -0
  15. dicube/codecs/jph/ojph_decode_complete.cp311-win32.pyd +0 -0
  16. dicube/codecs/jph/ojph_decode_complete.cp311-win_amd64.pyd +0 -0
  17. dicube/codecs/jph/ojph_decode_complete.cp38-win32.pyd +0 -0
  18. dicube/codecs/jph/ojph_decode_complete.cp38-win_amd64.pyd +0 -0
  19. dicube/codecs/jph/ojph_decode_complete.cp39-win32.pyd +0 -0
  20. dicube/codecs/jph/ojph_decode_complete.cp39-win_amd64.pyd +0 -0
  21. dicube/core/__init__.py +21 -0
  22. dicube/core/image.py +349 -0
  23. dicube/core/io.py +354 -0
  24. dicube/core/pixel_header.py +117 -0
  25. dicube/dicom/__init__.py +13 -0
  26. dicube/dicom/dcb_streaming.py +250 -0
  27. dicube/dicom/dicom_io.py +153 -0
  28. dicube/dicom/dicom_meta.py +740 -0
  29. dicube/dicom/dicom_status.py +259 -0
  30. dicube/dicom/dicom_tags.py +121 -0
  31. dicube/dicom/merge_utils.py +283 -0
  32. dicube/dicom/space_from_meta.py +70 -0
  33. dicube/exceptions.py +189 -0
  34. dicube/storage/__init__.py +17 -0
  35. dicube/storage/dcb_file.py +805 -0
  36. dicube/storage/pixel_utils.py +141 -0
  37. dicube/utils/__init__.py +6 -0
  38. dicube/validation.py +380 -0
  39. dicube-0.1.4.dist-info/METADATA +271 -0
  40. dicube-0.1.4.dist-info/RECORD +41 -0
  41. dicube-0.1.4.dist-info/WHEEL +5 -0
@@ -0,0 +1,117 @@
1
+ import json
2
+ from dataclasses import asdict, dataclass, field
3
+ from typing import Dict, Optional
4
+
5
+
6
+ @dataclass
7
+ class PixelDataHeader:
8
+ """Header class for storing pixel data information in medical images.
9
+
10
+ Stores metadata including:
11
+ - Rescale factors (slope/intercept)
12
+ - Original pixel data type
13
+ - Window settings (center/width)
14
+ - Value range (min/max)
15
+ - Additional metadata in EXTRAS
16
+
17
+ Attributes:
18
+ RESCALE_SLOPE (float): Slope for linear transformation.
19
+ RESCALE_INTERCEPT (float): Intercept for linear transformation.
20
+ PIXEL_DTYPE (str): Pixel data type string (after convert to dcb file).
21
+ ORIGINAL_PIXEL_DTYPE (str): Original pixel data type string (before convert to dcb file).
22
+ WINDOW_CENTER (float, optional): Window center value for display.
23
+ WINDOW_WIDTH (float, optional): Window width value for display.
24
+ MAX_VAL (float, optional): Maximum pixel value.
25
+ MIN_VAL (float, optional): Minimum pixel value.
26
+ EXTRAS (Dict[str, any]): Dictionary for additional metadata.
27
+ """
28
+
29
+ RESCALE_SLOPE: float = 1.0
30
+ RESCALE_INTERCEPT: float = 0.0
31
+ ORIGINAL_PIXEL_DTYPE: str = "uint16"
32
+ PIXEL_DTYPE: str = "uint16"
33
+ WINDOW_CENTER: Optional[float] = None
34
+ WINDOW_WIDTH: Optional[float] = None
35
+ MAX_VAL: Optional[float] = None
36
+ MIN_VAL: Optional[float] = None
37
+ EXTRAS: Dict[str, any] = field(default_factory=dict)
38
+
39
+ def to_dict(self) -> dict:
40
+ """Convert the header to a dictionary for serialization.
41
+
42
+ Merges EXTRAS field into the main dictionary and removes
43
+ the redundant EXTRAS key.
44
+
45
+ Returns:
46
+ dict: Dictionary representation of the header.
47
+ """
48
+ data = asdict(self)
49
+ data.update(self.EXTRAS) # Merge EXTRAS into dictionary
50
+ data.pop("EXTRAS", None) # Remove redundant EXTRAS field
51
+ return data
52
+
53
+ @classmethod
54
+ def from_dict(cls, d: dict):
55
+ """Create a PixelDataHeader from a dictionary.
56
+
57
+ Args:
58
+ d (dict): Dictionary containing header data.
59
+
60
+ Returns:
61
+ PixelDataHeader: A new instance with values from the dictionary.
62
+ """
63
+ rescale_slope = d.get("RESCALE_SLOPE", 1.0)
64
+ rescale_intercept = d.get("RESCALE_INTERCEPT", 0.0)
65
+ original_pixel_dtype = d.get("ORIGINAL_PIXEL_DTYPE", "uint16")
66
+ window_center = d.get("WINDOW_CENTER") # Defaults to None
67
+ window_width = d.get("WINDOW_WIDTH") # Defaults to None
68
+ max_val = d.get("MAX_VAL") # Defaults to None
69
+ min_val = d.get("MIN_VAL") # Defaults to None
70
+
71
+ # All other keys go into EXTRAS
72
+ extras = {
73
+ k: v
74
+ for k, v in d.items()
75
+ if k
76
+ not in {
77
+ "RESCALE_SLOPE",
78
+ "RESCALE_INTERCEPT",
79
+ "ORIGINAL_PIXEL_DTYPE",
80
+ "WINDOW_CENTER",
81
+ "WINDOW_WIDTH",
82
+ "MAX_VAL",
83
+ "MIN_VAL",
84
+ }
85
+ }
86
+
87
+ return cls(
88
+ RESCALE_SLOPE=rescale_slope,
89
+ RESCALE_INTERCEPT=rescale_intercept,
90
+ ORIGINAL_PIXEL_DTYPE=original_pixel_dtype,
91
+ WINDOW_CENTER=window_center,
92
+ WINDOW_WIDTH=window_width,
93
+ MAX_VAL=max_val,
94
+ MIN_VAL=min_val,
95
+ EXTRAS=extras,
96
+ )
97
+
98
+ def to_json(self) -> str:
99
+ """Serialize the header to a JSON string.
100
+
101
+ Returns:
102
+ str: JSON string representation of the header.
103
+ """
104
+ return json.dumps(self.to_dict())
105
+
106
+ @classmethod
107
+ def from_json(cls, json_str: str):
108
+ """Create a PixelDataHeader from a JSON string.
109
+
110
+ Args:
111
+ json_str (str): JSON string containing header data.
112
+
113
+ Returns:
114
+ PixelDataHeader: A new instance created from the JSON data.
115
+ """
116
+ obj_dict = json.loads(json_str)
117
+ return cls.from_dict(obj_dict)
@@ -0,0 +1,13 @@
1
+ from .dicom_meta import DicomMeta, SortMethod, read_dicom_dir
2
+ from .dicom_status import DicomStatus, get_dicom_status
3
+ from .dicom_tags import CommonTags
4
+ from .space_from_meta import get_space_from_DicomMeta
5
+ __all__ = [
6
+ "DicomMeta",
7
+ "read_dicom_dir",
8
+ "DicomStatus",
9
+ "get_dicom_status",
10
+ "CommonTags",
11
+ "SortMethod",
12
+ "get_space_from_DicomMeta",
13
+ ]
@@ -0,0 +1,250 @@
1
+ """
2
+ DCB Streaming Reader for PACS Viewer
3
+
4
+ Provides efficient streaming access to DCB files for on-demand DICOM frame delivery.
5
+ Keeps files open and metadata cached for low-latency responses.
6
+ """
7
+
8
+ import io
9
+ import struct
10
+ import warnings
11
+ from typing import Dict, Any
12
+
13
+ from pydicom import Dataset
14
+ from pydicom.dataset import FileMetaDataset
15
+ from pydicom.encaps import encapsulate
16
+ from pydicom.uid import generate_uid
17
+ import pydicom
18
+
19
+ from ..storage.dcb_file import DcbFile
20
+ from .dicom_io import save_dicom
21
+
22
+ # 定义所需的最低PyDicom版本
23
+ REQUIRED_PYDICOM_VERSION = "3.0.0"
24
+
25
+ class DcbStreamingReader:
26
+ """
27
+ PACS Viewer 的 dcb 文件流式读取器
28
+ 保持文件打开状态,支持快速随机访问帧数据
29
+
30
+ Example:
31
+ reader = DcbStreamingReader('study.dcbs')
32
+ dicom_bytes = reader.get_dicom_for_frame(50)
33
+ reader.close()
34
+ """
35
+
36
+ def __init__(self, dcb_file_path: str):
37
+ """
38
+ 初始化并预解析所有元数据
39
+
40
+ Args:
41
+ dcb_file_path: dcb 文件路径
42
+
43
+ Warnings:
44
+ UserWarning: 如果 PyDicom 版本低于 3.0.0,HTJ2K 解码可能无法正常工作
45
+ """
46
+ # 检查 PyDicom 版本
47
+ self._check_pydicom_version()
48
+
49
+ self.file_path = dcb_file_path
50
+ self.file_handle = None
51
+ self.transfer_syntax_uid = None
52
+
53
+ # 预解析的数据
54
+ self.header = None
55
+ self.dicom_meta = None
56
+ self.pixel_header = None
57
+ self.space = None
58
+
59
+ # 帧索引信息
60
+ self.frame_offsets = []
61
+ self.frame_lengths = []
62
+ self.frame_count = 0
63
+
64
+ # DcbFile 实例(用于读取元数据)
65
+ self.dcb_file = None
66
+
67
+ # 初始化
68
+ self._open_and_parse()
69
+
70
+ def _check_pydicom_version(self):
71
+ """
72
+ 检查 PyDicom 版本,如果不满足要求则发出警告
73
+
74
+ Warnings:
75
+ UserWarning: 如果 PyDicom 版本低于 3.0.0
76
+ """
77
+ current_version = pydicom.__version__
78
+ if current_version < REQUIRED_PYDICOM_VERSION:
79
+ warnings.warn(
80
+ f"DcbStreamingReader 需要 PyDicom >= {REQUIRED_PYDICOM_VERSION} 以完全支持 HTJ2K 传输语法。"
81
+ f"当前 PyDicom 版本为 {current_version},可能无法读取像素数据。"
82
+ f"写入功能不受影响,但其他应用读取时可能会出现问题。建议升级: pip install pydicom>={REQUIRED_PYDICOM_VERSION},需要 python 3.10 或更高版本",
83
+ UserWarning
84
+ )
85
+ self._has_pydicom_htj2k_support = False
86
+ else:
87
+ self._has_pydicom_htj2k_support = True
88
+
89
+ def _open_and_parse(self):
90
+ """打开文件并解析所有元数据"""
91
+ try:
92
+ # 1. 创建 DcbFile 实例(会自动检测文件类型)
93
+ self.dcb_file = DcbFile(self.file_path, mode='r')
94
+
95
+ # 2. 读取并缓存头部信息
96
+ self.header = self.dcb_file.header
97
+ self.frame_count = self.header['frame_count']
98
+
99
+ # 3. 读取并缓存元数据
100
+ self.dicom_meta = self.dcb_file.read_meta()
101
+ self.pixel_header = self.dcb_file.read_pixel_header()
102
+ self.space = self.dcb_file.read_space()
103
+
104
+ # 4. 获取 transfer syntax UID(从文件类型直接获取)
105
+ self.transfer_syntax_uid = self.dcb_file.get_transfer_syntax_uid()
106
+ if not self.transfer_syntax_uid:
107
+ # 如果文件类型没有定义 transfer syntax,使用默认的未压缩格式
108
+ self.transfer_syntax_uid = '1.2.840.10008.1.2.1' # Explicit VR Little Endian
109
+
110
+ # 5. 打开文件句柄用于读取帧数据
111
+ self.file_handle = open(self.file_path, 'rb')
112
+
113
+ # 6. 读取所有帧的 offset 和 length
114
+ self._read_frame_indices()
115
+
116
+ except Exception as e:
117
+ self.close()
118
+ raise RuntimeError(f"Failed to open and parse DCB file: {e}")
119
+
120
+ def _read_frame_indices(self):
121
+ """读取所有帧的偏移量和长度信息"""
122
+ self.file_handle.seek(self.header['frame_offsets_offset'])
123
+
124
+ # 读取 offsets
125
+ for _ in range(self.frame_count):
126
+ offset, = struct.unpack('<Q', self.file_handle.read(8))
127
+ self.frame_offsets.append(offset)
128
+
129
+ # 读取 lengths
130
+ self.file_handle.seek(self.header['frame_lengths_offset'])
131
+ for _ in range(self.frame_count):
132
+ length, = struct.unpack('<Q', self.file_handle.read(8))
133
+ self.frame_lengths.append(length)
134
+
135
+ def get_dicom_for_frame(self, frame_index: int) -> bytes:
136
+ """
137
+ 获取指定帧的 DICOM 数据
138
+
139
+ Args:
140
+ frame_index: 帧索引(0-based)
141
+
142
+ Returns:
143
+ bytes: 完整的 DICOM 文件数据
144
+
145
+ Raises:
146
+ IndexError: 如果 frame_index 超出范围
147
+ RuntimeError: 如果读取失败
148
+ """
149
+ # 验证索引
150
+ if not 0 <= frame_index < self.frame_count:
151
+ raise IndexError(f"Frame index {frame_index} out of range [0, {self.frame_count})")
152
+
153
+ try:
154
+ # 1. 读取该帧的编码数据
155
+ encoded_pixel_data = self._read_encoded_frame(frame_index)
156
+
157
+ # 2. 生成该帧的 DICOM Dataset
158
+ ds = self._create_dicom_dataset(frame_index, encoded_pixel_data)
159
+
160
+ # 3. 序列化为 DICOM 文件格式
161
+ return self._serialize_to_dicom_bytes(ds)
162
+
163
+ except Exception as e:
164
+ raise RuntimeError(f"Failed to create DICOM for frame {frame_index}: {e}")
165
+
166
+ def _read_encoded_frame(self, frame_index: int) -> bytes:
167
+ """直接读取指定帧的编码数据"""
168
+ offset = self.frame_offsets[frame_index]
169
+ length = self.frame_lengths[frame_index]
170
+
171
+ self.file_handle.seek(offset)
172
+ return self.file_handle.read(length)
173
+
174
+ def _create_dicom_dataset(self, frame_index: int, encoded_data: bytes) -> Dataset:
175
+ """快速创建 DICOM Dataset"""
176
+ # 1. 从缓存的 DicomMeta 获取该帧的元数据
177
+ if self.dicom_meta:
178
+ frame_meta_dict = self.dicom_meta.index(frame_index)
179
+ else:
180
+ frame_meta_dict = {}
181
+
182
+ # 2. 创建 Dataset
183
+ ds = Dataset.from_json(frame_meta_dict)
184
+
185
+ # 3. 创建并设置文件元信息
186
+ file_meta = FileMetaDataset()
187
+ file_meta.MediaStorageSOPClassUID = ds.get('SOPClassUID', '1.2.840.10008.5.1.4.1.1.2')
188
+ file_meta.MediaStorageSOPInstanceUID = ds.get('SOPInstanceUID', generate_uid())
189
+ file_meta.TransferSyntaxUID = self.transfer_syntax_uid
190
+ file_meta.ImplementationClassUID = generate_uid()
191
+
192
+ ds.file_meta = file_meta
193
+
194
+ # 4. 确保必要的 SOP 信息
195
+ if not hasattr(ds, 'SOPClassUID'):
196
+ ds.SOPClassUID = file_meta.MediaStorageSOPClassUID
197
+ if not hasattr(ds, 'SOPInstanceUID'):
198
+ ds.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID
199
+
200
+ # 5. 设置像素相关属性
201
+ if self.pixel_header:
202
+ ds.RescaleSlope = self.pixel_header.RESCALE_SLOPE
203
+ ds.RescaleIntercept = self.pixel_header.RESCALE_INTERCEPT
204
+
205
+ # 6. 设置像素数据(使用 encapsulated format for compressed data)
206
+ # 压缩数据需要封装
207
+ ds.PixelData = encapsulate([encoded_data])
208
+
209
+ return ds
210
+
211
+ def _serialize_to_dicom_bytes(self, ds: Dataset) -> bytes:
212
+ """将 Dataset 序列化为 DICOM 文件字节流"""
213
+ # 使用 BytesIO 在内存中创建 DICOM 文件
214
+ buffer = io.BytesIO()
215
+ save_dicom(ds, buffer)
216
+ buffer.seek(0)
217
+ return buffer.read()
218
+
219
+ def get_frame_count(self) -> int:
220
+ """获取总帧数"""
221
+ return self.frame_count
222
+
223
+ def get_metadata(self) -> Dict[str, Any]:
224
+ """获取缓存的元数据信息"""
225
+ return {
226
+ 'frame_count': self.frame_count,
227
+ 'pixel_header': self.pixel_header.to_dict() if self.pixel_header else {},
228
+ 'has_dicom_meta': self.dicom_meta is not None,
229
+ 'has_space': self.space is not None,
230
+ 'transfer_syntax': self.transfer_syntax_uid,
231
+ 'file_type': self.dcb_file.__class__.__name__,
232
+ }
233
+
234
+ def close(self):
235
+ """关闭文件句柄"""
236
+ if self.file_handle:
237
+ self.file_handle.close()
238
+ self.file_handle = None
239
+
240
+ def __enter__(self):
241
+ """支持 with 语句"""
242
+ return self
243
+
244
+ def __exit__(self, exc_type, exc_val, exc_tb):
245
+ """退出 with 语句时自动关闭"""
246
+ self.close()
247
+
248
+ def __del__(self):
249
+ """析构时确保文件关闭"""
250
+ self.close()
@@ -0,0 +1,153 @@
1
+ import os
2
+ import warnings
3
+ import inspect
4
+ from typing import List, Optional
5
+
6
+ import numpy as np
7
+ from pydicom import Dataset
8
+ from pydicom.dataset import FileMetaDataset
9
+ from pydicom.uid import JPEG2000, ExplicitVRLittleEndian, JPEG2000Lossless, generate_uid
10
+
11
+ from ..dicom.dicom_meta import DicomMeta
12
+
13
+
14
+ def prepare_output_dir(output_dir: str):
15
+ """Prepare output directory"""
16
+ if os.path.exists(output_dir):
17
+ import shutil
18
+
19
+ shutil.rmtree(output_dir)
20
+ os.makedirs(output_dir, exist_ok=True)
21
+
22
+
23
+ def create_file_meta(ds):
24
+ """Create file meta information"""
25
+ file_meta = FileMetaDataset()
26
+
27
+ MODALITY_SOP_CLASS_MAP = {
28
+ "CT": "1.2.840.10008.5.1.4.1.1.2",
29
+ "MR": "1.2.840.10008.5.1.4.1.1.4",
30
+ "US": "1.2.840.10008.5.1.4.1.1.6.1",
31
+ "PT": "1.2.840.10008.5.1.4.1.1.128",
32
+ "CR": "1.2.840.10008.5.1.4.1.1.1",
33
+ "DX": "1.2.840.10008.5.1.4.1.1.1.1",
34
+ "NM": "1.2.840.10008.5.1.4.1.1.20",
35
+ }
36
+
37
+ modality = ds.Modality if hasattr(ds, "Modality") else "CT"
38
+ default_sop_uid = MODALITY_SOP_CLASS_MAP.get(modality, MODALITY_SOP_CLASS_MAP["CT"])
39
+
40
+ file_meta.MediaStorageSOPClassUID = (
41
+ ds.SOPClassUID if hasattr(ds, "SOPClassUID") else default_sop_uid
42
+ )
43
+ file_meta.MediaStorageSOPInstanceUID = (
44
+ ds.SOPInstanceUID if hasattr(ds, "SOPInstanceUID") else generate_uid()
45
+ )
46
+ file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
47
+ file_meta.ImplementationClassUID = generate_uid()
48
+
49
+ return file_meta
50
+
51
+
52
+ def ensure_required_tags(ds):
53
+ """Ensure required DICOM tags exist"""
54
+ if not hasattr(ds, "SOPClassUID"):
55
+ ds.SOPClassUID = ds.file_meta.MediaStorageSOPClassUID
56
+ if not hasattr(ds, "SOPInstanceUID"):
57
+ ds.SOPInstanceUID = ds.file_meta.MediaStorageSOPInstanceUID
58
+
59
+
60
+ def set_dicom_pixel_attributes(img, ds):
61
+ """Set DICOM pixel attributes"""
62
+ if np.issubdtype(img.dtype, np.integer):
63
+ bits = img.dtype.itemsize * 8
64
+ ds.BitsAllocated = bits
65
+ ds.BitsStored = bits
66
+ ds.HighBit = bits - 1
67
+ ds.PixelRepresentation = 1 if np.issubdtype(img.dtype, np.signedinteger) else 0
68
+ else:
69
+ warnings.warn(f"Converting float dtype {img.dtype} to uint16")
70
+ img = img.astype(np.uint16)
71
+ ds.BitsAllocated = 16
72
+ ds.BitsStored = 16
73
+ ds.HighBit = 15
74
+ ds.PixelRepresentation = 0
75
+
76
+ ds.SamplesPerPixel = 1
77
+ return img
78
+
79
+
80
+ def create_dicom_dataset(meta_dict: dict, pixel_header):
81
+ """Create DICOM dataset"""
82
+ ds = Dataset.from_json(meta_dict)
83
+
84
+ if hasattr(ds, "file_meta"):
85
+ warnings.warn("Found original file metadata, will be overridden")
86
+
87
+ ds.file_meta = create_file_meta(ds)
88
+ ensure_required_tags(ds)
89
+ ds.RescaleSlope = pixel_header.RESCALE_SLOPE
90
+ ds.RescaleIntercept = pixel_header.RESCALE_INTERCEPT
91
+
92
+ return ds
93
+
94
+
95
+ def save_dicom(ds: Dataset, output_path: str):
96
+ """保存DICOM文件
97
+
98
+ 参数:
99
+ ds: DICOM数据集
100
+ output_path: 输出文件路径
101
+ """
102
+
103
+ sig = inspect.signature(Dataset.save_as)
104
+ if "enforce_file_format" in sig.parameters: # pydicom >= 3.0
105
+ ds.save_as(output_path, enforce_file_format=True)
106
+ else:
107
+ # 确保使用有效的传输语法UID
108
+ if hasattr(ds, 'file_meta') and hasattr(ds.file_meta, 'TransferSyntaxUID'):
109
+ # 检查是否有效,如果无效则替换为标准的ExplicitVRLittleEndian
110
+ try:
111
+ from pydicom.uid import UID
112
+ uid = UID(ds.file_meta.TransferSyntaxUID)
113
+ if not hasattr(uid, 'is_little_endian'):
114
+ ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
115
+ except (ValueError, AttributeError):
116
+ # 如果UID无效,使用标准的ExplicitVRLittleEndian
117
+ ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
118
+ ds.save_as(output_path, write_like_original=False)
119
+
120
+ def save_to_dicom_folder(
121
+ raw_images: np.ndarray,
122
+ dicom_meta: DicomMeta,
123
+ pixel_header,
124
+ output_dir: str,
125
+ filenames: Optional[List[str]] = None,
126
+ ):
127
+ """将图像数据保存为DICOM文件夹
128
+
129
+ 参数:
130
+ raw_images: 原始图像数据,可以是2D或3D数组
131
+ dicom_meta: DICOM元数据
132
+ pixel_header: 像素头信息
133
+ output_dir: 输出目录
134
+ filenames: 自定义文件名列表,如果为None则使用默认名称
135
+ """
136
+ prepare_output_dir(output_dir)
137
+
138
+ if raw_images.ndim == 2:
139
+ raw_images = raw_images[np.newaxis, ...]
140
+
141
+ for idx in range(len(raw_images)):
142
+ ds = create_dicom_dataset(dicom_meta.index(idx), pixel_header)
143
+ img = raw_images[idx]
144
+
145
+ if img.dtype != np.uint16:
146
+ img = set_dicom_pixel_attributes(img, ds)
147
+
148
+ ds.PixelData = img.tobytes()
149
+
150
+ output_path = os.path.join(
151
+ output_dir, filenames[idx] if filenames else f"slice_{idx:04d}.dcm"
152
+ )
153
+ save_dicom(ds, output_path)