-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* provide utility for adding impac private elements to spot scanning plans for interop testing purposes * made the script executable
- Loading branch information
1 parent
95fe95a
commit b7a9086
Showing
6 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
#!/usr/bin/env python | ||
import json | ||
import math | ||
import sys | ||
|
||
from pydicom import Dataset, datadict, dcmread, dcmwrite | ||
|
||
from impac_privates import impac_private_dict | ||
|
||
if __name__ == "__main__": | ||
energy_range_mm_dict = {} | ||
energy_string_range_dict = {} | ||
ion_plan_ds: Dataset = None | ||
# Values calculated on PSTAR NIST web site at integer energies in MeV | ||
# Assuming Linear Interpolation between integer energies is close enough | ||
# for non-clinical purposes, like testing workflow/interoperability. | ||
with open("proton_range_millimeters_water.json", "r") as f: | ||
energy_string_range_dict = json.load(f) | ||
energy_range_mm_dict = {int(x): y for x, y in energy_string_range_dict.items()} | ||
|
||
datadict.add_private_dict_entries("IMPAC", impac_private_dict) | ||
|
||
with open(sys.argv[1], "rb") as g: | ||
ion_plan_ds = dcmread(g) | ||
|
||
try: | ||
impac_block = ion_plan_ds.IonBeamSequence[0].private_block(0x300B, "IMPAC") | ||
if 0x04 in impac_block: | ||
print(f"File already contains IMPAC private for {impac_private_dict[0x300B1004][2]}") | ||
if len(sys.argv) < 2: # unless they want to force it with a -f or something | ||
sys.exit() | ||
except KeyError: | ||
pass # no impac block so we're good to go | ||
|
||
new_plan_name = sys.argv[1].removesuffix(".dcm") + "_ims_pvt.dcm" | ||
for beam in ion_plan_ds.IonBeamSequence: | ||
if beam.ScanMode not in ["MODULATED", "MODULATED_SPEC"]: | ||
print(f"Scan Mode is {beam.ScanMode} which isn't supported in this script/module") | ||
sys.exit() | ||
cp_sequence = beam.IonControlPointSequence | ||
cp_energy_list = [x.NominalBeamEnergy for x in cp_sequence] | ||
max_energy = max(cp_energy_list) | ||
min_energy = min(cp_energy_list) | ||
ceiling_max_energy = math.ceil(max_energy) | ||
floor_max_energy = math.floor(max_energy) | ||
ceiling_max_weight = max_energy - floor_max_energy | ||
|
||
ceiling_min_energy = math.ceil(min_energy) | ||
floor_min_energy = math.floor(min_energy) | ||
ceiling_min_weight = min_energy - floor_min_energy | ||
|
||
# this isn't taking in to account a Range Shifter or a Range Modulator | ||
# but for simple interoperability and workflow testing, it will do. | ||
distal_range_millimeters = ( | ||
energy_range_mm_dict[floor_max_energy] * (1.0 - ceiling_max_weight) | ||
+ energy_range_mm_dict[ceiling_max_energy] * ceiling_max_weight | ||
) | ||
proximal_range_millimeters = ( | ||
energy_range_mm_dict[floor_min_energy] * (1.0 - ceiling_min_weight) | ||
+ energy_range_mm_dict[ceiling_min_energy] * ceiling_min_weight | ||
) | ||
sobp_equivalent_mm = round(distal_range_millimeters - proximal_range_millimeters, ndigits=1) | ||
|
||
beam_max_radius_squared = 0 | ||
for cp in cp_sequence: | ||
x_values = cp.ScanSpotPositionMap[0::2] | ||
y_values = cp.ScanSpotPositionMap[1::2] | ||
tuple_list = [] | ||
r_squared_list = [x * x + y * y for x, y in zip(x_values, y_values)] | ||
max_r_squared = max(r_squared_list) | ||
if beam_max_radius_squared < max_r_squared: | ||
beam_max_radius_squared = max_r_squared | ||
|
||
beam_max_radius = math.sqrt(beam_max_radius_squared) | ||
new_private_block = beam.private_block(0x300B, "IMPAC", create=True) | ||
new_private_block.add_new(0x04, "FL", round(distal_range_millimeters, 1)) | ||
new_private_block.add_new(0x02, "FL", round(beam_max_radius, 1)) | ||
new_private_block.add_new(0x0E, "FL", round(sobp_equivalent_mm, 1)) | ||
|
||
dcmwrite( | ||
new_plan_name, ion_plan_ds, write_like_original=True | ||
) # assume the input was ExplicitLittleEndian for later tasks, see convert_to_explicit_little_endian.py | ||
print(f"Wrote updated file to {new_plan_name}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#!/usr/bin/env python | ||
""" if the data contains private elements that aren't known to pydicom, | ||
and the file is transmitted to qrscp or some other system that doesn't handle unknown privates | ||
when using *implicit* little endian, | ||
then this conversion is needed to avoid data loss later on. | ||
@author: stuartswerdloff | ||
""" | ||
import sys | ||
from pathlib import Path | ||
|
||
from pydicom import dcmread | ||
from pydicom.uid import ExplicitVRLittleEndian | ||
|
||
|
||
def convert(filename: str | Path, output_filename: str | Path): | ||
try: | ||
f = open(filename, "rb") | ||
ds = dcmread(f, force=True) | ||
|
||
f.close() | ||
ds.is_little_endian = True | ||
|
||
# if the data contains private elements that aren't known to pydicom, | ||
# and the file is transmitted to qrscp or some other system that doesn't handle unknown privates | ||
# when using *implicit* little endian, | ||
# then this is needed to avoid data loss later on. | ||
ds.is_implicit_VR = False | ||
|
||
ds.ensure_file_meta() | ||
ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian | ||
ds.fix_meta_info() | ||
ds.save_as(output_filename, write_like_original=False) | ||
except IOError: | ||
print("Cannot read input file {0!s}".format(filename)) | ||
raise | ||
|
||
|
||
if __name__ == "__main__": | ||
if len(sys.argv) < 3: | ||
print(f"{sys.argv[0]} input_file_path output_file_path") | ||
sys.exit() | ||
|
||
filename = sys.argv[1] | ||
output_filename = sys.argv[2] | ||
print(filename) | ||
try: | ||
convert(filename=filename, output_filename=output_filename) | ||
except IOError: | ||
print("Cannot read input file {0!s}".format(filename)) | ||
sys.exit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
impac_private_dict = { | ||
0x30091001: ("FL", "1", "Measured Distal Target Distance", ""), # noqa | ||
0x30091002: ("FL", "1", "Specified Primary Ambient Meterset", ""), # noqa | ||
0x30091003: ("FL", "1", "Delivered Primary Ambient Meterset", ""), # noqa | ||
0x30091004: ("FL", "1", "Current Temperature", ""), # noqa | ||
0x30091005: ("FL", "1", "Current Pressure", ""), # noqa | ||
0x30091006: ("FL", "1", "TP Correction Factor", ""), # noqa | ||
0x30091007: ("FL", "1", "Measured Uncollimated Field Diameter", ""), # noqa | ||
0x30091008: ("FL", "1", "Measured SOBP Width", ""), # noqa | ||
0x30091010: ("FL", "1", "Table Top Vertical Correction", ""), # noqa | ||
0x30091011: ("FL", "1", "Table Top Longitudinal Correction", ""), # noqa | ||
0x30091012: ("FL", "1", "Table Top Lateral Correction", ""), # noqa | ||
0x30091013: ("FL", "1", "Patient Support Angle Correction", ""), # noqa | ||
0x30091014: ("FL", "1", "Patient Support Pitch Angle Correction", ""), # noqa | ||
0x30091015: ("FL", "1", "Patient Support Roll Angle Correction", ""), # noqa | ||
0x30091016: ("IS", "1", "Number of Paintings Fully Delivered", ""), # noqa | ||
0x30091017: ("IS", "1", "Treatment Termination Scan Spot Index", ""), # noqa | ||
0x30091047: ("FL", "2-n", "Line Scan Metersets Delivered", ""), # noqa | ||
0x300B1001: ("FL", "1", "Distal Target Distance Tolerance", ""), # noqa | ||
0x300B1002: ("FL", "1", "Maximum Collimated Field Diameter", ""), # noqa | ||
0x300B1003: ("CS", "1", "Beam Check Flag", ""), # noqa | ||
0x300B1004: ("FL", "1", "Planned Distal Target Distance", ""), # noqa | ||
0x300B1005: ("CS", "1", "Treatment Delivery Status", ""), # noqa | ||
0x300B1006: ("CS", "1", "Treatment Machine Mode", ""), # noqa | ||
0x300B1007: ("CS", "1", "Position Correction Flag", ""), # noqa | ||
0x300B1008: ("SH", "1", "Beam Line Data Table Version", ""), # noqa | ||
0x300B1009: ("CS", "1", "Respiratory Gating Flag", ""), # noqa | ||
0x300B100A: ("FL", "1", "Respiratory Gating Cycle", ""), # noqa | ||
0x300B100B: ("FL", "1", "Flat Top Length", ""), # noqa | ||
0x300B100C: ("FL", "1", "Spill Length", ""), # noqa | ||
0x300B100D: ("FL", "1", "Uncollimated Field Diameter Tolerance", ""), # noqa | ||
0x300B100E: ("FL", "1", "Nominal SOBP Width", ""), # noqa | ||
0x300B1011: ("FL", "1", "Nominal SOBP Width Tolerance", ""), # noqa | ||
0x300B1012: ("FL", "1", "TP Corrected Meterset Tolerance", ""), # noqa | ||
0x300B1013: ("IS", "1", "Number of Pieces", ""), # noqa | ||
0x300B1014: ("FL", "1-n", "Change Check Data Before", ""), # noqa | ||
0x300B1015: ("FL", "1-n", "Change Check Data After", ""), # noqa | ||
0x300B1016: ("FL", "1", "Beam Intensity", ""), # noqa | ||
0x300B1017: ("FL", "1", "Peak Range", ""), # noqa | ||
0x300B1018: ("DS", "1", "Planned Patient Support Angle", ""), # noqa | ||
0x300B101A: ("FL", "1", "Planned Table Top Roll Angle", ""), # noqa | ||
0x300B1020: ("DS", "2", "Respiratory Phase Gating Duty Cycle", ""), # noqa | ||
0x300B1090: ("SH", "2", "Line Spot Tune ID", ""), # noqa | ||
0x300B1092: ("IS", "1", "Number of Line Scan Spot Positions", ""), # noqa | ||
0x300B1094: ("FL", "2-n", "Line Scan Position Map", ""), # noqa | ||
0x300B1096: ("FL", "2-n", "Line Scan Meterset Weights", ""), # noqa | ||
0x300B1098: ("FL", "2", "Line Scanning Spot Size", ""), # noqa | ||
0x300B109A: ("IS", "1", "Number of Line Scan Paintings", ""), # noqa | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"1": 0.02, "2": 0.08, "3": 0.15, "4": 0.25, "5": 0.36, "6": 0.5, "7": 0.65, "8": 0.83, "9": 1.02, "10": 1.23, "11": 1.45, "12": 1.7, "13": 1.96, "14": 2.24, "15": 2.54, "16": 2.85, "17": 3.17, "18": 3.52, "19": 3.88, "20": 4.25, "21": 4.64, "22": 5.05, "23": 5.47, "24": 5.91, "25": 6.36, "26": 6.83, "27": 7.31, "28": 7.8, "29": 8.31, "30": 8.84, "31": 9.38, "32": 9.93, "33": 10.5, "34": 11.08, "35": 11.68, "36": 12.29, "37": 12.91, "38": 13.55, "39": 14.2, "40": 14.86, "41": 15.54, "42": 16.23, "43": 16.94, "44": 17.66, "45": 18.39, "46": 19.13, "47": 19.89, "48": 20.66, "49": 21.44, "50": 22.24, "51": 23.05, "52": 23.87, "53": 24.7, "54": 25.55, "55": 26.41, "56": 27.28, "57": 28.16, "58": 29.06, "59": 29.97, "60": 30.89, "61": 31.82, "62": 32.76, "63": 33.72, "64": 34.69, "65": 35.67, "66": 36.66, "67": 37.66, "68": 38.68, "69": 39.71, "70": 40.75, "71": 41.8, "72": 42.86, "73": 43.93, "74": 45.02, "75": 46.11, "76": 47.22, "77": 48.34, "78": 49.47, "79": 50.61, "80": 51.76, "81": 52.93, "82": 54.1, "83": 55.29, "84": 56.48, "85": 57.69, "86": 58.91, "87": 60.14, "88": 61.38, "89": 62.63, "90": 63.89, "91": 65.16, "92": 66.44, "93": 67.73, "94": 69.04, "95": 70.35, "96": 71.67, "97": 73.01, "98": 74.35, "99": 75.71, "100": 77.07, "101": 78.45, "102": 79.83, "103": 81.23, "104": 82.64, "105": 84.05, "106": 85.48, "107": 86.91, "108": 88.36, "109": 89.81, "110": 91.28, "111": 92.75, "112": 94.24, "113": 95.73, "114": 97.24, "115": 98.75, "116": 100.3, "117": 101.8, "118": 103.4, "119": 104.9, "120": 106.5, "121": 108.0, "122": 109.6, "123": 111.2, "124": 112.8, "125": 114.4, "126": 116.0, "127": 117.7, "128": 119.3, "129": 120.9, "130": 122.6, "131": 124.3, "132": 125.9, "133": 127.6, "134": 129.3, "135": 131.0, "136": 132.7, "137": 134.4, "138": 136.2, "139": 137.9, "140": 139.6, "141": 141.4, "142": 143.1, "143": 144.9, "144": 146.7, "145": 148.5, "146": 150.3, "147": 152.1, "148": 153.9, "149": 155.7, "150": 157.6, "151": 159.4, "152": 161.2, "153": 163.1, "154": 165.0, "155": 166.8, "156": 168.7, "157": 170.6, "158": 172.5, "159": 174.4, "160": 176.3, "161": 178.2, "162": 180.2, "163": 182.1, "164": 184.1, "165": 186.0, "166": 188.0, "167": 189.9, "168": 191.9, "169": 193.9, "170": 195.9, "171": 197.9, "172": 199.9, "173": 201.9, "174": 204.0, "175": 206.0, "176": 208.0, "177": 210.1, "178": 212.1, "179": 214.2, "180": 216.3, "181": 218.3, "182": 220.4, "183": 222.5, "184": 224.6, "185": 226.7, "186": 228.9, "187": 231.0, "188": 233.1, "189": 235.3, "190": 237.4, "191": 239.6, "192": 241.7, "193": 243.9, "194": 246.1, "195": 248.3, "196": 250.4, "197": 252.6, "198": 254.8, "199": 257.1, "200": 259.3, "201": 261.5, "202": 263.7, "203": 266.0, "204": 268.2, "205": 270.5, "206": 272.8, "207": 275.0, "208": 277.3, "209": 279.6, "210": 281.9, "211": 284.2, "212": 286.5, "213": 288.8, "214": 291.1, "215": 293.4, "216": 295.8, "217": 298.1, "218": 300.4, "219": 302.8, "220": 305.2, "221": 307.5, "222": 309.9, "223": 312.3, "224": 314.7, "225": 317.1, "226": 319.4, "227": 321.9, "228": 324.3, "229": 326.7, "230": 329.1, "231": 331.5, "232": 334.0, "233": 336.4, "234": 338.9, "235": 341.3, "236": 343.8, "237": 346.3, "238": 348.7, "239": 351.2, "240": 353.7, "241": 356.2, "242": 358.7, "243": 361.2, "244": 363.7, "245": 366.3, "246": 368.8, "247": 371.3, "248": 373.9, "249": 376.4, "250": 379.0, "251": 381.5, "252": 384.1, "253": 386.6, "254": 389.2, "255": 391.8, "256": 394.4, "257": 397.0, "258": 399.6, "259": 402.2, "260": 404.8, "261": 407.4, "262": 410.0, "263": 412.7, "264": 415.3, "265": 417.9, "266": 420.6, "267": 423.2, "268": 425.9, "269": 428.6, "270": 431.2, "271": 433.9, "272": 436.6, "273": 439.3, "274": 442.0, "275": 444.7, "276": 447.4, "277": 450.1, "278": 452.8, "279": 455.5, "280": 458.3, "281": 461.0, "282": 463.7, "283": 466.5, "284": 469.2, "285": 472.0, "286": 474.7, "287": 477.5, "288": 480.3, "289": 483.0, "290": 485.8, "291": 488.6, "292": 491.4, "293": 494.2, "294": 497.0, "295": 499.8, "296": 502.6, "297": 505.4, "298": 508.3, "299": 511.1, "300": 513.9, "301": 516.8, "302": 519.6, "303": 522.5, "304": 525.3, "305": 528.2, "306": 531.1, "307": 533.9, "308": 536.8, "309": 539.7, "310": 542.6, "311": 545.5, "312": 548.4, "313": 551.3, "314": 554.2, "315": 557.1, "316": 560.0, "317": 562.9, "318": 565.9, "319": 568.8, "320": 571.7, "321": 574.7, "322": 577.6, "323": 580.6, "324": 583.5, "325": 586.5, "326": 589.5, "327": 592.4, "328": 595.4, "329": 598.4, "330": 601.4} |