163 lines
5.0 KiB
Python
Executable File
163 lines
5.0 KiB
Python
Executable File
# Copyright 2019 pydicom authors. See LICENSE file for details.
|
|
"""Pydicom command line interface program for `pydicom show`"""
|
|
|
|
import argparse
|
|
from collections.abc import Callable
|
|
|
|
from pydicom.dataset import Dataset
|
|
from pydicom.cli.main import filespec_help, filespec_parser
|
|
|
|
|
|
def add_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
subparser = subparsers.add_parser(
|
|
"show", description="Display all or part of a DICOM file"
|
|
)
|
|
subparser.add_argument("filespec", help=filespec_help, type=filespec_parser)
|
|
subparser.add_argument(
|
|
"-x",
|
|
"--exclude-private",
|
|
help="Don't show private data elements",
|
|
action="store_true",
|
|
)
|
|
subparser.add_argument(
|
|
"-t", "--top", help="Only show top level", action="store_true"
|
|
)
|
|
subparser.add_argument(
|
|
"-q",
|
|
"--quiet",
|
|
help="Only show basic information",
|
|
action="store_true",
|
|
)
|
|
|
|
subparser.set_defaults(func=do_command)
|
|
|
|
|
|
def do_command(args: argparse.Namespace) -> None:
|
|
if len(args.filespec) != 1:
|
|
raise NotImplementedError("Show can only work on a single DICOM file input")
|
|
|
|
ds, element_val = args.filespec[0]
|
|
if not element_val:
|
|
element_val = ds
|
|
|
|
if args.exclude_private:
|
|
ds.remove_private_tags()
|
|
|
|
if args.quiet and isinstance(element_val, Dataset):
|
|
show_quiet(element_val)
|
|
elif args.top and isinstance(element_val, Dataset):
|
|
print(element_val.top())
|
|
else:
|
|
print(str(element_val))
|
|
|
|
|
|
def SOPClassname(ds: Dataset) -> str | None:
|
|
class_uid = ds.get("SOPClassUID")
|
|
if class_uid is None:
|
|
return None
|
|
return f"SOPClassUID: {class_uid.name}"
|
|
|
|
|
|
def quiet_rtplan(ds: Dataset) -> str | None:
|
|
if "BeamSequence" not in ds:
|
|
return None
|
|
|
|
plan_label = ds.get("RTPlanLabel")
|
|
plan_name = ds.get("RTPlanName")
|
|
line = f"Plan Label: {plan_label} "
|
|
if plan_name:
|
|
line += f"Plan Name: {plan_name}"
|
|
lines = [line]
|
|
|
|
if "FractionGroupSequence" in ds: # it should be, is mandatory
|
|
for fraction_group in ds.FractionGroupSequence:
|
|
fraction_group_num = fraction_group.get("FractionGroupNumber", "")
|
|
descr = fraction_group.get("FractionGroupDescription", "")
|
|
fractions = fraction_group.get("NumberOfFractionsPlanned")
|
|
fxn_info = f"{fractions} fraction(s) planned" if fractions else ""
|
|
lines.append(f"Fraction Group {fraction_group_num} {descr} {fxn_info}")
|
|
num_brachy = fraction_group.get("NumberOfBrachyApplicationSetups")
|
|
lines.append(f" Brachy Application Setups: {num_brachy}")
|
|
for refd_beam in fraction_group.ReferencedBeamSequence:
|
|
ref_num = refd_beam.get("ReferencedBeamNumber")
|
|
dose = refd_beam.get("BeamDose")
|
|
mu = refd_beam.get("BeamMeterset")
|
|
line = f" Beam {ref_num} "
|
|
if dose or mu:
|
|
line += f"Dose {dose} Meterset {mu}"
|
|
lines.append(line)
|
|
|
|
for beam in ds.BeamSequence:
|
|
beam_num = beam.get("BeamNumber")
|
|
beam_name = beam.get("BeamName")
|
|
beam_type = beam.get("BeamType")
|
|
beam_delivery = beam.get("TreatmentDeliveryType")
|
|
beam_radtype = beam.get("RadiationType")
|
|
line = (
|
|
f"Beam {beam_num} '{beam_name}' {beam_delivery} "
|
|
f"{beam_type} {beam_radtype}"
|
|
)
|
|
|
|
if beam_type == "STATIC":
|
|
cp = beam.ControlPointSequence[0]
|
|
if cp:
|
|
energy = cp.get("NominalBeamEnergy")
|
|
gantry = cp.get("GantryAngle")
|
|
bld = cp.get("BeamLimitingDeviceAngle")
|
|
couch = cp.get("PatientSupportAngle")
|
|
line += f" energy {energy} gantry {gantry}, coll {bld}, couch {couch}"
|
|
|
|
wedges = beam.get("NumberOfWedges")
|
|
comps = beam.get("NumberOfCompensators")
|
|
boli = beam.get("NumberOfBoli")
|
|
blocks = beam.get("NumberOfBlocks")
|
|
|
|
line += f" ({wedges} wedges, {comps} comps, {boli} boli, {blocks} blocks)"
|
|
|
|
lines.append(line)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def quiet_image(ds: Dataset) -> str | None:
|
|
if "SOPClassUID" not in ds or "Image Storage" not in ds.SOPClassUID.name:
|
|
return None
|
|
|
|
results = [
|
|
f"{name}: {ds.get(name, 'N/A')}"
|
|
for name in [
|
|
"BitsStored",
|
|
"Modality",
|
|
"Rows",
|
|
"Columns",
|
|
"SliceLocation",
|
|
]
|
|
]
|
|
return "\n".join(results)
|
|
|
|
|
|
# Items to show in quiet mode
|
|
# Item can be a callable or a DICOM keyword
|
|
quiet_items: list[Callable[[Dataset], str | None] | str] = [
|
|
SOPClassname,
|
|
"PatientName",
|
|
"PatientID",
|
|
# Images
|
|
"StudyID",
|
|
"StudyDate",
|
|
"StudyTime",
|
|
"StudyDescription",
|
|
quiet_image,
|
|
quiet_rtplan,
|
|
]
|
|
|
|
|
|
def show_quiet(ds: Dataset) -> None:
|
|
for item in quiet_items:
|
|
if callable(item):
|
|
result = item(ds)
|
|
if result:
|
|
print(result)
|
|
else:
|
|
print(f"{item}: {ds.get(item, 'N/A')}")
|