|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -from typing import Any |
| 5 | +from typing import TYPE_CHECKING, Any, ClassVar, Literal |
6 | 6 |
|
7 | 7 | from csa_header.ascii import CsaAsciiHeader |
8 | 8 | from csa_header.exceptions import CsaReadError |
9 | 9 | from csa_header.messages import INVALID_CHECK_BIT, READ_OVERREACH, TOO_MANY_ITEMS |
10 | 10 | from csa_header.unpacker import Unpacker |
11 | 11 | from csa_header.utils import VR_TO_TYPE, decode_latin1, strip_to_null |
12 | 12 |
|
| 13 | +if TYPE_CHECKING: |
| 14 | + import pydicom # Only imported for type hints |
| 15 | + |
13 | 16 |
|
14 | 17 | class CsaHeader: |
15 | 18 | """ |
@@ -54,6 +57,12 @@ class CsaHeader: |
54 | 57 | #: ASCII header tag names. |
55 | 58 | ASCII_HEADER_TAGS: frozenset[str] = frozenset({"MrPhoenixProtocol"}) |
56 | 59 |
|
| 60 | + #: Mapping of CSA header types to their DICOM private tag addresses. |
| 61 | + CSA_TAGS: ClassVar[dict[str, tuple[int, int]]] = { |
| 62 | + "image": (0x0029, 0x1010), # CSA Image Header Info |
| 63 | + "series": (0x0029, 0x1020), # CSA Series Header Info |
| 64 | + } |
| 65 | + |
57 | 66 | def __init__(self, raw: bytes): |
58 | 67 | """ |
59 | 68 | Initialize a new `CsaHeader` instance. |
@@ -352,3 +361,127 @@ def is_type_2(self) -> bool: |
352 | 361 | CSA type 2 or not |
353 | 362 | """ |
354 | 363 | return self._csa_type == self.CSA_TYPE_2 |
| 364 | + |
| 365 | + @staticmethod |
| 366 | + def _extract_csa_bytes( |
| 367 | + dcm_data: pydicom.Dataset, |
| 368 | + csa_type: str, |
| 369 | + ) -> bytes | None: |
| 370 | + """ |
| 371 | + Extract raw CSA header bytes from a DICOM dataset. |
| 372 | +
|
| 373 | + This is a private helper method that locates and extracts the raw |
| 374 | + CSA header data from a DICOM dataset's private tags. |
| 375 | +
|
| 376 | + Parameters |
| 377 | + ---------- |
| 378 | + dcm_data : pydicom.Dataset |
| 379 | + DICOM dataset containing Siemens CSA header information |
| 380 | + csa_type : str |
| 381 | + Type of CSA header to extract ('image' or 'series') |
| 382 | +
|
| 383 | + Returns |
| 384 | + ------- |
| 385 | + bytes or None |
| 386 | + Raw CSA header bytes, or None if the tag is not present |
| 387 | + """ |
| 388 | + # Get the tag address for the requested CSA type |
| 389 | + tag = CsaHeader.CSA_TAGS[csa_type] |
| 390 | + |
| 391 | + # Check if the tag exists in the dataset |
| 392 | + if tag not in dcm_data: |
| 393 | + return None |
| 394 | + |
| 395 | + # Extract and return the value |
| 396 | + element = dcm_data[tag] |
| 397 | + if element.value is None: |
| 398 | + return None |
| 399 | + |
| 400 | + return bytes(element.value) |
| 401 | + |
| 402 | + @classmethod |
| 403 | + def from_dicom( |
| 404 | + cls, |
| 405 | + dcm_data: pydicom.Dataset, |
| 406 | + csa_type: Literal["image", "series"] = "image", |
| 407 | + ) -> CsaHeader | None: |
| 408 | + """ |
| 409 | + Extract and parse CSA header directly from a DICOM dataset. |
| 410 | +
|
| 411 | + This method implements the DICOM private tag search protocol to locate |
| 412 | + and extract CSA headers from Siemens DICOM files. The implementation is |
| 413 | + inspired by nibabel's ``get_csa_header()`` function, adapted to csa_header's |
| 414 | + API design. |
| 415 | +
|
| 416 | + For more information on nibabel's CSA header support, see: |
| 417 | + https://github.com/nipy/nibabel |
| 418 | +
|
| 419 | + Parameters |
| 420 | + ---------- |
| 421 | + dcm_data : pydicom.Dataset |
| 422 | + DICOM dataset from a Siemens MRI scanner |
| 423 | + csa_type : {'image', 'series'}, default='image' |
| 424 | + Type of CSA header to extract: |
| 425 | +
|
| 426 | + - ``'image'`` : CSA Image Header Info (0x0029, 0x1010) |
| 427 | + - ``'series'`` : CSA Series Header Info (0x0029, 0x1020) |
| 428 | +
|
| 429 | + Returns |
| 430 | + ------- |
| 431 | + CsaHeader or None |
| 432 | + CsaHeader instance containing the raw CSA data, or None if the |
| 433 | + specified CSA header is not present in the dataset. Call ``.read()`` |
| 434 | + on the returned instance to get the parsed dictionary. |
| 435 | +
|
| 436 | + Raises |
| 437 | + ------ |
| 438 | + ValueError |
| 439 | + If ``csa_type`` is not 'image' or 'series' |
| 440 | +
|
| 441 | + Examples |
| 442 | + -------- |
| 443 | + >>> import pydicom |
| 444 | + >>> from csa_header import CsaHeader |
| 445 | + >>> |
| 446 | + >>> # Load DICOM file |
| 447 | + >>> dcm = pydicom.dcmread('siemens_scan.dcm') |
| 448 | + >>> |
| 449 | + >>> # Extract image CSA header |
| 450 | + >>> csa_header = CsaHeader.from_dicom(dcm, 'image') |
| 451 | + >>> if csa_header: |
| 452 | + ... csa_dict = csa_header.read() |
| 453 | + ... print(f"Found {len(csa_dict)} CSA tags") |
| 454 | + >>> |
| 455 | + >>> # Extract series CSA header |
| 456 | + >>> csa_header = CsaHeader.from_dicom(dcm, 'series') |
| 457 | + >>> if csa_header: |
| 458 | + ... csa_dict = csa_header.read() |
| 459 | + ... protocol = csa_dict.get('MrPhoenixProtocol') |
| 460 | +
|
| 461 | + Notes |
| 462 | + ----- |
| 463 | + This is a convenience method that combines DICOM tag extraction with |
| 464 | + CSA header parsing. It is equivalent to: |
| 465 | +
|
| 466 | + >>> raw_bytes = dcm[(0x0029, 0x1010)].value # for 'image' |
| 467 | + >>> csa_header = CsaHeader(raw_bytes) |
| 468 | + >>> csa_dict = csa_header.read() |
| 469 | +
|
| 470 | + See Also |
| 471 | + -------- |
| 472 | + CsaHeader.read : Parse CSA header from raw bytes |
| 473 | + """ |
| 474 | + # Validate csa_type parameter |
| 475 | + csa_type_lower = csa_type.lower() |
| 476 | + if csa_type_lower not in cls.CSA_TAGS: |
| 477 | + valid_types = ", ".join(repr(t) for t in cls.CSA_TAGS.keys()) |
| 478 | + msg = f"Invalid csa_type: {csa_type!r}. Must be one of: {valid_types}" |
| 479 | + raise ValueError(msg) |
| 480 | + |
| 481 | + # Extract raw CSA bytes |
| 482 | + raw_bytes = cls._extract_csa_bytes(dcm_data, csa_type_lower) |
| 483 | + if raw_bytes is None: |
| 484 | + return None |
| 485 | + |
| 486 | + # Create and return CsaHeader instance |
| 487 | + return cls(raw_bytes) |
0 commit comments