dicube 0.1.4__cp312-cp312-macosx_11_0_arm64.whl → 0.2.0__cp312-cp312-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.
- dicube/__init__.py +49 -15
- dicube/core/image.py +6 -6
- dicube/core/io.py +69 -21
- dicube/core/pixel_header.py +50 -47
- dicube/dicom/dcb_streaming.py +56 -58
- dicube/dicom/dicom_io.py +16 -16
- dicube/storage/dcb_file.py +68 -49
- dicube/storage/pixel_utils.py +151 -33
- {dicube-0.1.4.dist-info → dicube-0.2.0.dist-info}/METADATA +1 -1
- {dicube-0.1.4.dist-info → dicube-0.2.0.dist-info}/RECORD +11 -11
- {dicube-0.1.4.dist-info → dicube-0.2.0.dist-info}/WHEEL +0 -0
dicube/dicom/dcb_streaming.py
CHANGED
@@ -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
|
-
#
|
22
|
+
# Required minimum PyDicom version
|
23
23
|
REQUIRED_PYDICOM_VERSION = "3.0.0"
|
24
24
|
|
25
25
|
class DcbStreamingReader:
|
26
|
-
"""
|
27
|
-
|
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:
|
40
|
+
dcb_file_path: Path to DCB file
|
42
41
|
|
43
42
|
Warnings:
|
44
|
-
UserWarning:
|
43
|
+
UserWarning: If PyDicom version is below 3.0.0, HTJ2K decoding may not work properly
|
45
44
|
"""
|
46
|
-
#
|
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:
|
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
|
81
|
-
f"
|
82
|
-
f"
|
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.
|
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.
|
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
|
-
#
|
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.
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
136
|
+
Get DICOM data for the specified frame.
|
138
137
|
|
139
138
|
Args:
|
140
|
-
frame_index:
|
139
|
+
frame_index: Frame index (0-based)
|
141
140
|
|
142
141
|
Returns:
|
143
|
-
bytes:
|
142
|
+
bytes: Complete DICOM file data
|
144
143
|
|
145
144
|
Raises:
|
146
|
-
IndexError:
|
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.
|
156
|
+
# 2. Generate DICOM Dataset for the frame
|
158
157
|
ds = self._create_dicom_dataset(frame_index, encoded_pixel_data)
|
159
158
|
|
160
|
-
# 3.
|
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
|
-
"""
|
176
|
-
# 1.
|
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.
|
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.
|
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.
|
203
|
-
ds.RescaleIntercept = self.pixel_header.
|
201
|
+
ds.RescaleSlope = self.pixel_header.RescaleSlope
|
202
|
+
ds.RescaleIntercept = self.pixel_header.RescaleIntercept
|
204
203
|
|
205
|
-
# 6.
|
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
|
-
"""
|
213
|
-
#
|
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
|
-
"""
|
239
|
+
"""Support with statement."""
|
242
240
|
return self
|
243
241
|
|
244
242
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
245
|
-
"""
|
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.
|
90
|
-
ds.RescaleIntercept = pixel_header.
|
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
|
-
"""
|
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
|
-
#
|
107
|
+
# Ensure valid transfer syntax UID
|
108
108
|
if hasattr(ds, 'file_meta') and hasattr(ds.file_meta, 'TransferSyntaxUID'):
|
109
|
-
#
|
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
|
-
#
|
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
|
-
"""
|
127
|
+
"""Save image data as DICOM folder.
|
128
128
|
|
129
|
-
|
130
|
-
raw_images:
|
131
|
-
dicom_meta: DICOM
|
132
|
-
pixel_header:
|
133
|
-
output_dir:
|
134
|
-
filenames:
|
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
|
|
dicube/storage/dcb_file.py
CHANGED
@@ -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
|
-
#
|
269
|
-
|
270
|
-
|
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
|
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
|
-
|
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
|
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
|
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.
|
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
|
-
|
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.
|
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
|
646
|
-
|
647
|
-
|
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
|
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
|
-
|
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
|
662
|
-
|
663
|
-
|
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
|
670
|
-
|
671
|
-
Raises:
|
672
|
-
NotImplementedError: This method must be implemented by subclasses.
|
685
|
+
np.ndarray: The decoded image frame.
|
673
686
|
"""
|
674
|
-
|
687
|
+
return np.array([], dtype=self.header_data['pixel_header'].OriginalPixelDtype)
|
675
688
|
|
676
|
-
def read_dicom_status(self) ->
|
689
|
+
def read_dicom_status(self) -> DicomStatus:
|
677
690
|
"""Read DICOM status information from the file.
|
678
691
|
|
679
692
|
Returns:
|
680
|
-
|
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
|
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"
|