dicube 0.1.4__cp39-cp39-macosx_11_0_arm64.whl → 0.2.2__cp39-cp39-macosx_11_0_arm64.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.
@@ -19,13 +19,13 @@ import pydicom
19
19
  from ..storage.dcb_file import DcbFile
20
20
  from .dicom_io import save_dicom
21
21
 
22
- # 定义所需的最低PyDicom版本
22
+ # Required minimum PyDicom version
23
23
  REQUIRED_PYDICOM_VERSION = "3.0.0"
24
24
 
25
25
  class DcbStreamingReader:
26
- """
27
- PACS Viewer 的 dcb 文件流式读取器
28
- 保持文件打开状态,支持快速随机访问帧数据
26
+ """DCB file streaming reader for PACS Viewer.
27
+
28
+ Keeps files open and supports fast random access to frame data.
29
29
 
30
30
  Example:
31
31
  reader = DcbStreamingReader('study.dcbs')
@@ -34,52 +34,51 @@ class DcbStreamingReader:
34
34
  """
35
35
 
36
36
  def __init__(self, dcb_file_path: str):
37
- """
38
- 初始化并预解析所有元数据
37
+ """Initialize and preparse all metadata.
39
38
 
40
39
  Args:
41
- dcb_file_path: dcb 文件路径
40
+ dcb_file_path: Path to DCB file
42
41
 
43
42
  Warnings:
44
- UserWarning: 如果 PyDicom 版本低于 3.0.0HTJ2K 解码可能无法正常工作
43
+ UserWarning: If PyDicom version is below 3.0.0, HTJ2K decoding may not work properly
45
44
  """
46
- # 检查 PyDicom 版本
45
+ # Check PyDicom version
47
46
  self._check_pydicom_version()
48
47
 
49
48
  self.file_path = dcb_file_path
50
49
  self.file_handle = None
51
50
  self.transfer_syntax_uid = None
52
51
 
53
- # 预解析的数据
52
+ # Pre-parsed data
54
53
  self.header = None
55
54
  self.dicom_meta = None
56
55
  self.pixel_header = None
57
56
  self.space = None
58
57
 
59
- # 帧索引信息
58
+ # Frame index information
60
59
  self.frame_offsets = []
61
60
  self.frame_lengths = []
62
61
  self.frame_count = 0
63
62
 
64
- # DcbFile 实例(用于读取元数据)
63
+ # DcbFile instance (for reading metadata)
65
64
  self.dcb_file = None
66
65
 
67
- # 初始化
66
+ # Initialize
68
67
  self._open_and_parse()
69
68
 
70
69
  def _check_pydicom_version(self):
71
- """
72
- 检查 PyDicom 版本,如果不满足要求则发出警告
70
+ """Check PyDicom version and warn if requirements not met.
73
71
 
74
72
  Warnings:
75
- UserWarning: 如果 PyDicom 版本低于 3.0.0
73
+ UserWarning: If PyDicom version is below 3.0.0
76
74
  """
77
75
  current_version = pydicom.__version__
78
76
  if current_version < REQUIRED_PYDICOM_VERSION:
79
77
  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 或更高版本",
78
+ f"DcbStreamingReader requires PyDicom >= {REQUIRED_PYDICOM_VERSION} for full HTJ2K transfer syntax support. "
79
+ f"Current PyDicom version is {current_version}, which may not be able to read pixel data. "
80
+ f"Write functionality is not affected, but other applications may have issues reading. "
81
+ f"Recommended upgrade: pip install pydicom>={REQUIRED_PYDICOM_VERSION}, requires python 3.10 or higher",
83
82
  UserWarning
84
83
  )
85
84
  self._has_pydicom_htj2k_support = False
@@ -87,30 +86,30 @@ class DcbStreamingReader:
87
86
  self._has_pydicom_htj2k_support = True
88
87
 
89
88
  def _open_and_parse(self):
90
- """打开文件并解析所有元数据"""
89
+ """Open file and parse all metadata."""
91
90
  try:
92
- # 1. 创建 DcbFile 实例(会自动检测文件类型)
91
+ # 1. Create DcbFile instance (will auto-detect file type)
93
92
  self.dcb_file = DcbFile(self.file_path, mode='r')
94
93
 
95
- # 2. 读取并缓存头部信息
94
+ # 2. Read and cache header information
96
95
  self.header = self.dcb_file.header
97
96
  self.frame_count = self.header['frame_count']
98
97
 
99
- # 3. 读取并缓存元数据
98
+ # 3. Read and cache metadata
100
99
  self.dicom_meta = self.dcb_file.read_meta()
101
100
  self.pixel_header = self.dcb_file.read_pixel_header()
102
101
  self.space = self.dcb_file.read_space()
103
102
 
104
- # 4. 获取 transfer syntax UID(从文件类型直接获取)
103
+ # 4. Get transfer syntax UID (directly from file type)
105
104
  self.transfer_syntax_uid = self.dcb_file.get_transfer_syntax_uid()
106
105
  if not self.transfer_syntax_uid:
107
- # 如果文件类型没有定义 transfer syntax,使用默认的未压缩格式
106
+ # If file type doesn't define transfer syntax, use default uncompressed format
108
107
  self.transfer_syntax_uid = '1.2.840.10008.1.2.1' # Explicit VR Little Endian
109
108
 
110
- # 5. 打开文件句柄用于读取帧数据
109
+ # 5. Open file handle for reading frame data
111
110
  self.file_handle = open(self.file_path, 'rb')
112
111
 
113
- # 6. 读取所有帧的 offset length
112
+ # 6. Read all frame offsets and lengths
114
113
  self._read_frame_indices()
115
114
 
116
115
  except Exception as e:
@@ -118,15 +117,15 @@ class DcbStreamingReader:
118
117
  raise RuntimeError(f"Failed to open and parse DCB file: {e}")
119
118
 
120
119
  def _read_frame_indices(self):
121
- """读取所有帧的偏移量和长度信息"""
120
+ """Read all frame offset and length information."""
122
121
  self.file_handle.seek(self.header['frame_offsets_offset'])
123
122
 
124
- # 读取 offsets
123
+ # Read offsets
125
124
  for _ in range(self.frame_count):
126
125
  offset, = struct.unpack('<Q', self.file_handle.read(8))
127
126
  self.frame_offsets.append(offset)
128
127
 
129
- # 读取 lengths
128
+ # Read lengths
130
129
  self.file_handle.seek(self.header['frame_lengths_offset'])
131
130
  for _ in range(self.frame_count):
132
131
  length, = struct.unpack('<Q', self.file_handle.read(8))
@@ -134,37 +133,37 @@ class DcbStreamingReader:
134
133
 
135
134
  def get_dicom_for_frame(self, frame_index: int) -> bytes:
136
135
  """
137
- 获取指定帧的 DICOM 数据
136
+ Get DICOM data for the specified frame.
138
137
 
139
138
  Args:
140
- frame_index: 帧索引(0-based
139
+ frame_index: Frame index (0-based)
141
140
 
142
141
  Returns:
143
- bytes: 完整的 DICOM 文件数据
142
+ bytes: Complete DICOM file data
144
143
 
145
144
  Raises:
146
- IndexError: 如果 frame_index 超出范围
147
- RuntimeError: 如果读取失败
145
+ IndexError: If frame_index is out of range
146
+ RuntimeError: If reading fails
148
147
  """
149
- # 验证索引
148
+ # Validate index
150
149
  if not 0 <= frame_index < self.frame_count:
151
150
  raise IndexError(f"Frame index {frame_index} out of range [0, {self.frame_count})")
152
151
 
153
152
  try:
154
- # 1. 读取该帧的编码数据
153
+ # 1. Read encoded data for the frame
155
154
  encoded_pixel_data = self._read_encoded_frame(frame_index)
156
155
 
157
- # 2. 生成该帧的 DICOM Dataset
156
+ # 2. Generate DICOM Dataset for the frame
158
157
  ds = self._create_dicom_dataset(frame_index, encoded_pixel_data)
159
158
 
160
- # 3. 序列化为 DICOM 文件格式
159
+ # 3. Serialize to DICOM file format
161
160
  return self._serialize_to_dicom_bytes(ds)
162
161
 
163
162
  except Exception as e:
164
163
  raise RuntimeError(f"Failed to create DICOM for frame {frame_index}: {e}")
165
164
 
166
165
  def _read_encoded_frame(self, frame_index: int) -> bytes:
167
- """直接读取指定帧的编码数据"""
166
+ """Read encoded data for the specified frame directly."""
168
167
  offset = self.frame_offsets[frame_index]
169
168
  length = self.frame_lengths[frame_index]
170
169
 
@@ -172,17 +171,17 @@ class DcbStreamingReader:
172
171
  return self.file_handle.read(length)
173
172
 
174
173
  def _create_dicom_dataset(self, frame_index: int, encoded_data: bytes) -> Dataset:
175
- """快速创建 DICOM Dataset"""
176
- # 1. 从缓存的 DicomMeta 获取该帧的元数据
174
+ """Quickly create DICOM Dataset."""
175
+ # 1. Get metadata for the frame from cached DicomMeta
177
176
  if self.dicom_meta:
178
177
  frame_meta_dict = self.dicom_meta.index(frame_index)
179
178
  else:
180
179
  frame_meta_dict = {}
181
180
 
182
- # 2. 创建 Dataset
181
+ # 2. Create Dataset
183
182
  ds = Dataset.from_json(frame_meta_dict)
184
183
 
185
- # 3. 创建并设置文件元信息
184
+ # 3. Create and set file metadata
186
185
  file_meta = FileMetaDataset()
187
186
  file_meta.MediaStorageSOPClassUID = ds.get('SOPClassUID', '1.2.840.10008.5.1.4.1.1.2')
188
187
  file_meta.MediaStorageSOPInstanceUID = ds.get('SOPInstanceUID', generate_uid())
@@ -191,37 +190,36 @@ class DcbStreamingReader:
191
190
 
192
191
  ds.file_meta = file_meta
193
192
 
194
- # 4. 确保必要的 SOP 信息
193
+ # 4. Ensure necessary SOP information
195
194
  if not hasattr(ds, 'SOPClassUID'):
196
195
  ds.SOPClassUID = file_meta.MediaStorageSOPClassUID
197
196
  if not hasattr(ds, 'SOPInstanceUID'):
198
197
  ds.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID
199
198
 
200
- # 5. 设置像素相关属性
199
+ # 5. Set pixel-related attributes
201
200
  if self.pixel_header:
202
- ds.RescaleSlope = self.pixel_header.RESCALE_SLOPE
203
- ds.RescaleIntercept = self.pixel_header.RESCALE_INTERCEPT
201
+ ds.RescaleSlope = self.pixel_header.RescaleSlope
202
+ ds.RescaleIntercept = self.pixel_header.RescaleIntercept
204
203
 
205
- # 6. 设置像素数据(使用 encapsulated format for compressed data
206
- # 压缩数据需要封装
204
+ # 6. Set pixel data (using encapsulated format for compressed data)
207
205
  ds.PixelData = encapsulate([encoded_data])
208
206
 
209
207
  return ds
210
208
 
211
209
  def _serialize_to_dicom_bytes(self, ds: Dataset) -> bytes:
212
- """ Dataset 序列化为 DICOM 文件字节流"""
213
- # 使用 BytesIO 在内存中创建 DICOM 文件
210
+ """Serialize Dataset to DICOM file byte stream."""
211
+ # Use BytesIO to create DICOM file in memory
214
212
  buffer = io.BytesIO()
215
213
  save_dicom(ds, buffer)
216
214
  buffer.seek(0)
217
215
  return buffer.read()
218
216
 
219
217
  def get_frame_count(self) -> int:
220
- """获取总帧数"""
218
+ """Get total number of frames."""
221
219
  return self.frame_count
222
220
 
223
221
  def get_metadata(self) -> Dict[str, Any]:
224
- """获取缓存的元数据信息"""
222
+ """Get cached metadata information."""
225
223
  return {
226
224
  'frame_count': self.frame_count,
227
225
  'pixel_header': self.pixel_header.to_dict() if self.pixel_header else {},
@@ -232,19 +230,19 @@ class DcbStreamingReader:
232
230
  }
233
231
 
234
232
  def close(self):
235
- """关闭文件句柄"""
233
+ """Close file handle."""
236
234
  if self.file_handle:
237
235
  self.file_handle.close()
238
236
  self.file_handle = None
239
237
 
240
238
  def __enter__(self):
241
- """支持 with 语句"""
239
+ """Support with statement."""
242
240
  return self
243
241
 
244
242
  def __exit__(self, exc_type, exc_val, exc_tb):
245
- """退出 with 语句时自动关闭"""
243
+ """Automatically close when exiting with statement."""
246
244
  self.close()
247
245
 
248
246
  def __del__(self):
249
- """析构时确保文件关闭"""
247
+ """Ensure file is closed on destruction."""
250
248
  self.close()
dicube/dicom/dicom_io.py CHANGED
@@ -86,34 +86,34 @@ def create_dicom_dataset(meta_dict: dict, pixel_header):
86
86
 
87
87
  ds.file_meta = create_file_meta(ds)
88
88
  ensure_required_tags(ds)
89
- ds.RescaleSlope = pixel_header.RESCALE_SLOPE
90
- ds.RescaleIntercept = pixel_header.RESCALE_INTERCEPT
89
+ ds.RescaleSlope = pixel_header.RescaleSlope
90
+ ds.RescaleIntercept = pixel_header.RescaleIntercept
91
91
 
92
92
  return ds
93
93
 
94
94
 
95
95
  def save_dicom(ds: Dataset, output_path: str):
96
- """保存DICOM文件
96
+ """Save DICOM file.
97
97
 
98
- 参数:
99
- ds: DICOM数据集
100
- output_path: 输出文件路径
98
+ Args:
99
+ ds: DICOM dataset
100
+ output_path: Output file path
101
101
  """
102
102
 
103
103
  sig = inspect.signature(Dataset.save_as)
104
104
  if "enforce_file_format" in sig.parameters: # pydicom >= 3.0
105
105
  ds.save_as(output_path, enforce_file_format=True)
106
106
  else:
107
- # 确保使用有效的传输语法UID
107
+ # Ensure valid transfer syntax UID
108
108
  if hasattr(ds, 'file_meta') and hasattr(ds.file_meta, 'TransferSyntaxUID'):
109
- # 检查是否有效,如果无效则替换为标准的ExplicitVRLittleEndian
109
+ # Check if valid, replace with standard ExplicitVRLittleEndian if invalid
110
110
  try:
111
111
  from pydicom.uid import UID
112
112
  uid = UID(ds.file_meta.TransferSyntaxUID)
113
113
  if not hasattr(uid, 'is_little_endian'):
114
114
  ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
115
115
  except (ValueError, AttributeError):
116
- # 如果UID无效,使用标准的ExplicitVRLittleEndian
116
+ # If UID is invalid, use standard ExplicitVRLittleEndian
117
117
  ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
118
118
  ds.save_as(output_path, write_like_original=False)
119
119
 
@@ -124,14 +124,14 @@ def save_to_dicom_folder(
124
124
  output_dir: str,
125
125
  filenames: Optional[List[str]] = None,
126
126
  ):
127
- """将图像数据保存为DICOM文件夹
127
+ """Save image data as DICOM folder.
128
128
 
129
- 参数:
130
- raw_images: 原始图像数据,可以是2D3D数组
131
- dicom_meta: DICOM元数据
132
- pixel_header: 像素头信息
133
- output_dir: 输出目录
134
- filenames: 自定义文件名列表,如果为None则使用默认名称
129
+ Args:
130
+ raw_images: Raw image data, can be 2D or 3D array
131
+ dicom_meta: DICOM metadata
132
+ pixel_header: Pixel header information
133
+ output_dir: Output directory
134
+ filenames: Custom filename list, if None default names will be used
135
135
  """
136
136
  prepare_output_dir(output_dir)
137
137
 
@@ -86,12 +86,14 @@ class DcbFile:
86
86
  MAGIC (bytes): Magic bytes for file identification.
87
87
  VERSION (int): File format version.
88
88
  TRANSFER_SYNTAX_UID (str, optional): DICOM transfer syntax UID.
89
+ FILE_EXTENSION (str): File extension for this format.
89
90
  """
90
91
 
91
92
  HEADER_STRUCT = "<8sI13Q"
92
93
  MAGIC = b"DCMCUBE\x00"
93
94
  VERSION = 1
94
95
  TRANSFER_SYNTAX_UID = None # Base class has no specific transfer syntax
96
+ FILE_EXTENSION = ".dcb" # Default extension
95
97
 
96
98
  def __init__(self, filename: str, mode: str = "r"):
97
99
  """Initialize a DCB file object.
@@ -112,6 +114,10 @@ class DcbFile:
112
114
  suggestion="Use 'r' for reading, 'w' for writing, or 'a' for appending"
113
115
  )
114
116
 
117
+ # For write mode, ensure filename has correct extension
118
+ if mode == "w":
119
+ filename = self._ensure_correct_extension(filename)
120
+
115
121
  self.filename = filename
116
122
  self.mode = mode
117
123
  self._header = None # Delay reading header until needed
@@ -119,6 +125,19 @@ class DcbFile:
119
125
  if os.path.exists(filename) and mode in ("r", "a"):
120
126
  self._read_header_and_check_type()
121
127
 
128
+ def _ensure_correct_extension(self, filename: str) -> str:
129
+ """Ensure the filename has the correct extension for this file type.
130
+
131
+ Args:
132
+ filename (str): The original filename.
133
+
134
+ Returns:
135
+ str: The filename with correct extension.
136
+ """
137
+ if not filename.endswith(self.FILE_EXTENSION):
138
+ return filename + self.FILE_EXTENSION
139
+ return filename
140
+
122
141
  def _read_header_and_check_type(self):
123
142
  """Read file header and determine the correct subclass."""
124
143
  try:
@@ -265,12 +284,9 @@ class DcbFile:
265
284
  else:
266
285
  dicom_status = DicomStatus.CONSISTENT
267
286
 
268
- # Handle both enum and string values for dicom_status
269
- if isinstance(dicom_status, DicomStatus):
270
- dicom_status_bin = dicom_status.value.encode("utf-8")
271
- else:
272
- # If it's already a string, encode it directly
273
- dicom_status_bin = dicom_status.encode("utf-8")
287
+ # Convert DicomStatus enum to string for storage
288
+ # If None was provided, dicom_status will be a DicomStatus enum at this point
289
+ dicom_status_bin = dicom_status.value.encode("utf-8")
274
290
 
275
291
  # Process dicom_meta
276
292
  if dicom_meta:
@@ -297,17 +313,20 @@ class DcbFile:
297
313
  'pixel_header_bin': pixel_header_bin
298
314
  }
299
315
 
300
- def _encode_frames(self, images: List, num_threads: int = 4):
316
+ def _encode_frames(self, images: List):
301
317
  """Encode frames using parallel or serial processing.
302
318
 
303
319
  Args:
304
320
  images (List): List of frames to encode.
305
- num_threads (int): Number of worker threads for parallel encoding.
306
321
 
307
322
  Returns:
308
323
  List[bytes]: List of encoded frame data.
309
324
  """
310
- if num_threads is not None and num_threads > 1:
325
+ # Import get_num_threads function to avoid circular import
326
+ from .. import get_num_threads
327
+ num_threads = get_num_threads()
328
+
329
+ if num_threads > 1:
311
330
  # Parallel encoding
312
331
  with ThreadPoolExecutor(max_workers=num_threads) as executor:
313
332
  encoded_blobs = list(
@@ -408,9 +427,8 @@ class DcbFile:
408
427
  images: List, # Can be List[np.ndarray] or List[Tuple] for ROI data
409
428
  pixel_header: PixelDataHeader,
410
429
  dicom_meta: Optional[DicomMeta] = None,
430
+ dicom_status: DicomStatus = DicomStatus.CONSISTENT,
411
431
  space: Optional[Space] = None,
412
- num_threads: int = 4,
413
- dicom_status: Optional[DicomStatus] = None,
414
432
  ):
415
433
  """Write image data and metadata to a DCB file.
416
434
 
@@ -422,9 +440,8 @@ class DcbFile:
422
440
  or List[Tuple[np.ndarray, np.ndarray, np.ndarray]] for ROI files.
423
441
  pixel_header (PixelDataHeader): PixelDataHeader instance containing pixel metadata.
424
442
  dicom_meta (DicomMeta, optional): DICOM metadata. Defaults to None.
443
+ dicom_status (DicomStatus): DICOM status enumeration. Defaults to DicomStatus.CONSISTENT.
425
444
  space (Space, optional): Spatial information. Defaults to None.
426
- num_threads (int): Number of worker threads for parallel encoding. Defaults to 4.
427
- dicom_status (str, optional): DICOM status string value. Defaults to None.
428
445
  """
429
446
  if images is None:
430
447
  images = []
@@ -436,7 +453,7 @@ class DcbFile:
436
453
  )
437
454
 
438
455
  # Encode frames
439
- encoded_frames = self._encode_frames(images, num_threads)
456
+ encoded_frames = self._encode_frames(images)
440
457
 
441
458
  # Write file structure
442
459
  header_size = struct.calcsize(self.HEADER_STRUCT)
@@ -560,42 +577,41 @@ class DcbFile:
560
577
  pixel_header_json = pixel_header_bin.decode("utf-8")
561
578
  return HeaderClass.from_json(pixel_header_json)
562
579
 
563
- def read_images(self, num_threads: int = 4):
580
+ def read_images(self):
564
581
  """Read all image frames from the file.
565
-
566
- Args:
567
- num_threads (int): Number of worker threads for parallel decoding.
568
- Defaults to 4.
569
-
582
+
570
583
  Returns:
571
584
  List[np.ndarray] or np.ndarray: The decoded image frames. If the number of frames is 1,
572
585
  returns a single numpy array, otherwise returns a list of arrays.
573
586
  """
587
+ # Import get_num_threads function to avoid circular import
588
+ from .. import get_num_threads
589
+
574
590
  hdr = self.header
575
591
  frame_count = hdr["frame_count"]
576
-
592
+
577
593
  if frame_count == 0:
578
594
  # No frames to read
579
595
  pixel_header = self.read_pixel_header()
580
- return np.array([], dtype=pixel_header.ORIGINAL_PIXEL_DTYPE)
596
+ return np.array([], dtype=pixel_header.OriginalPixelDtype)
581
597
 
582
598
  # Read frame offsets and lengths
583
599
  frame_offsets_offset = hdr["frame_offsets_offset"]
584
600
  frame_offsets_length = hdr["frame_offsets_length"]
585
601
  frame_lengths_offset = hdr["frame_lengths_offset"]
586
602
  frame_lengths_length = hdr["frame_lengths_length"]
587
-
603
+
588
604
  with open(self.filename, "rb") as f:
589
605
  # Read frame offsets
590
606
  f.seek(frame_offsets_offset)
591
607
  frame_offsets_bin = f.read(frame_offsets_length)
592
608
  frame_offsets = struct.unpack(f"<{frame_count}Q", frame_offsets_bin)
593
-
609
+
594
610
  # Read frame lengths
595
611
  f.seek(frame_lengths_offset)
596
612
  frame_lengths_bin = f.read(frame_lengths_length)
597
613
  frame_lengths = struct.unpack(f"<{frame_count}Q", frame_lengths_bin)
598
-
614
+
599
615
  # Read each frame data
600
616
  frame_data_list = []
601
617
  for offset, length in zip(frame_offsets, frame_lengths):
@@ -610,8 +626,9 @@ class DcbFile:
610
626
 
611
627
  # Decode frames (with parallelization if needed)
612
628
  frames = []
613
-
614
- if num_threads is not None and num_threads > 1 and frame_count > 1:
629
+
630
+ num_threads = get_num_threads()
631
+ if num_threads > 1 and frame_count > 1:
615
632
  # Parallel decoding
616
633
  with ThreadPoolExecutor(max_workers=num_threads) as executor:
617
634
  frames = list(
@@ -633,7 +650,7 @@ class DcbFile:
633
650
  if len(frames) == 0:
634
651
  # Return empty array if no frames were decoded
635
652
  pixel_header = self.read_pixel_header()
636
- return np.array([], dtype=pixel_header.ORIGINAL_PIXEL_DTYPE)
653
+ return np.array([], dtype=pixel_header.OriginalPixelDtype)
637
654
  elif len(frames) == 1:
638
655
  # Return single frame directly
639
656
  return frames[0]
@@ -642,49 +659,45 @@ class DcbFile:
642
659
  return frames
643
660
 
644
661
  def _encode_one_frame(self, frame_data: np.ndarray) -> bytes:
645
- """Encode a single frame of image data.
646
-
647
- This is a placeholder method to be implemented by subclasses.
662
+ """Encode a single frame to bytes.
663
+
664
+ Default implementation returns empty bytes.
665
+ Subclasses should override this method to implement specific encoding.
648
666
 
649
667
  Args:
650
- frame_data (np.ndarray): The frame data to encode.
651
-
668
+ frame_data (np.ndarray): The image frame to encode.
669
+
652
670
  Returns:
653
671
  bytes: The encoded frame data.
654
-
655
- Raises:
656
- NotImplementedError: This method must be implemented by subclasses.
657
672
  """
658
- raise NotImplementedError("Subclass must implement _encode_one_frame")
673
+ return np.array([], dtype=self.pixel_header.OriginalPixelDtype)
659
674
 
660
675
  def _decode_one_frame(self, bytes) -> np.ndarray:
661
- """Decode a single frame of image data.
662
-
663
- This is a placeholder method to be implemented by subclasses.
676
+ """Decode a single frame from bytes.
677
+
678
+ Default implementation returns an empty array with the correct data type.
679
+ Subclasses should override this method to implement specific decoding.
664
680
 
665
681
  Args:
666
682
  bytes (bytes): The encoded frame data.
667
683
 
668
684
  Returns:
669
- np.ndarray: The decoded frame data.
670
-
671
- Raises:
672
- NotImplementedError: This method must be implemented by subclasses.
685
+ np.ndarray: The decoded image frame.
673
686
  """
674
- raise NotImplementedError("Subclass must implement _decode_one_frame")
687
+ return np.array([], dtype=self.header_data['pixel_header'].OriginalPixelDtype)
675
688
 
676
- def read_dicom_status(self) -> str:
689
+ def read_dicom_status(self) -> DicomStatus:
677
690
  """Read DICOM status information from the file.
678
691
 
679
692
  Returns:
680
- str: The DICOM status string, or DicomStatus.CONSISTENT.value if not present.
693
+ DicomStatus: The DICOM status enum value, or DicomStatus.CONSISTENT if not present.
681
694
  """
682
695
  hdr = self.header
683
696
  dicom_status_offset = hdr["dicom_status_offset"]
684
697
  dicom_status_length = hdr["dicom_status_length"]
685
698
 
686
699
  if dicom_status_length == 0:
687
- return DicomStatus.CONSISTENT.value
700
+ return DicomStatus.CONSISTENT
688
701
 
689
702
  with open(self.filename, "rb") as f:
690
703
  f.seek(dicom_status_offset)
@@ -715,12 +728,14 @@ class DcbSFile(DcbFile):
715
728
  VERSION (int): File format version.
716
729
  TRANSFER_SYNTAX_UID (str): DICOM transfer syntax UID for HTJ2K Lossless.
717
730
  CODEC_NAME (str): Codec name used for compression.
731
+ FILE_EXTENSION (str): File extension for speed-optimized format.
718
732
  """
719
733
 
720
734
  MAGIC = b"DCMCUBES"
721
735
  VERSION = 1
722
736
  TRANSFER_SYNTAX_UID = "1.2.840.10008.1.2.4.201" # HTJ2K Lossless
723
737
  CODEC_NAME = "jph"
738
+ FILE_EXTENSION = ".dcbs"
724
739
 
725
740
  def _encode_one_frame(self, frame_data: np.ndarray) -> bytes:
726
741
  """Encode a single frame using the HTJ2K codec.
@@ -778,12 +793,14 @@ class DcbAFile(DcbFile):
778
793
  VERSION (int): File format version.
779
794
  TRANSFER_SYNTAX_UID (str, optional): DICOM transfer syntax UID, set when codec is selected.
780
795
  CODEC_NAME (str, optional): Codec name, set when codec is selected.
796
+ FILE_EXTENSION (str): File extension for archive-optimized format.
781
797
  """
782
798
 
783
799
  MAGIC = b"DCMCUBEA"
784
800
  VERSION = 1
785
801
  TRANSFER_SYNTAX_UID = None # To be defined when codec is selected
786
802
  CODEC_NAME = None # To be defined when codec is selected
803
+ FILE_EXTENSION = ".dcba"
787
804
 
788
805
 
789
806
  class DcbLFile(DcbFile):
@@ -797,9 +814,11 @@ class DcbLFile(DcbFile):
797
814
  VERSION (int): File format version.
798
815
  TRANSFER_SYNTAX_UID (str, optional): DICOM transfer syntax UID, set when codec is selected.
799
816
  CODEC_NAME (str, optional): Codec name, set when codec is selected.
817
+ FILE_EXTENSION (str): File extension for lossy compression format.
800
818
  """
801
819
 
802
820
  MAGIC = b"DCMCUBEL"
803
821
  VERSION = 1
804
822
  TRANSFER_SYNTAX_UID = None # To be defined when codec is selected
805
- CODEC_NAME = None # To be defined when codec is selected
823
+ CODEC_NAME = None # To be defined when codec is selected
824
+ FILE_EXTENSION = ".dcbl"