|
6 | 6 |
|
7 | 7 | import unittest
|
8 | 8 | import os
|
9 |
| -from pydicom.dataset import Dataset |
| 9 | +from pydicom.dataset import Dataset, FileMetaDataset |
10 | 10 | from pydicom.sequence import Sequence
|
| 11 | +from pydicom.uid import generate_uid |
11 | 12 | from numpy import arange
|
| 13 | +import numpy |
12 | 14 | from numpy.testing import assert_allclose
|
13 | 15 | from .util import fake_rtdose, fake_ss
|
14 | 16 | from dicompylercore import dicomparser, dvhcalc
|
@@ -255,6 +257,75 @@ def test_dvh_calculation_with_interpolation_between_planes(self):
|
255 | 257 | # Mean dose to structure
|
256 | 258 | self.assertAlmostEqual(dvh.mean, 6.4767105)
|
257 | 259 |
|
| 260 | + def test_histogram_preserves_final_bin(self): |
| 261 | + """Regression test for maxdose computation.""" |
| 262 | + # Create minimal RT Dose dataset with max dose slightly above an |
| 263 | + # integer bin boundary so rounding down would truncate it. |
| 264 | + dose = Dataset() |
| 265 | + dose.SOPClassUID = "1.2.840.10008.5.1.4.1.1.481.2" |
| 266 | + dose.SOPInstanceUID = generate_uid() |
| 267 | + dose.Modality = "RTDOSE" |
| 268 | + dose.Rows = 1 |
| 269 | + dose.Columns = 1 |
| 270 | + dose.NumberOfFrames = 1 |
| 271 | + dose.GridFrameOffsetVector = [0.0] |
| 272 | + dose.FrameIncrementPointer = (0x3004, 0x000c) |
| 273 | + dose.ImagePositionPatient = [0.0, 0.0, 0.0] |
| 274 | + dose.ImageOrientationPatient = [1, 0, 0, 0, 1, 0] |
| 275 | + dose.PixelSpacing = [1.0, 1.0] |
| 276 | + dose.SamplesPerPixel = 1 |
| 277 | + dose.PhotometricInterpretation = "MONOCHROME2" |
| 278 | + dose.BitsAllocated = 32 |
| 279 | + dose.BitsStored = 32 |
| 280 | + dose.HighBit = 31 |
| 281 | + dose.PixelRepresentation = 0 |
| 282 | + dose.DoseUnits = "GY" |
| 283 | + dose.DoseType = "PHYSICAL" |
| 284 | + dose.DoseSummationType = "PLAN" |
| 285 | + dose.DoseGridScaling = 0.00001 |
| 286 | + |
| 287 | + arr = numpy.array([[[53001]]], dtype=numpy.uint32) |
| 288 | + dose.PixelData = arr.tobytes() |
| 289 | + |
| 290 | + file_meta = FileMetaDataset() |
| 291 | + file_meta.FileMetaInformationVersion = b"\x00\x01" |
| 292 | + file_meta.MediaStorageSOPClassUID = dose.SOPClassUID |
| 293 | + file_meta.MediaStorageSOPInstanceUID = dose.SOPInstanceUID |
| 294 | + file_meta.TransferSyntaxUID = "1.2.840.10008.1.2" |
| 295 | + dose.file_meta = file_meta |
| 296 | + dose.is_implicit_VR = True |
| 297 | + dose.is_little_endian = True |
| 298 | + |
| 299 | + # Define a simple square structure covering the single dose voxel |
| 300 | + structure = { |
| 301 | + "id": 1, |
| 302 | + "name": "square", |
| 303 | + "thickness": 1, |
| 304 | + "planes": { |
| 305 | + 0.0: [ |
| 306 | + { |
| 307 | + "type": "CLOSED_PLANAR", |
| 308 | + "num_points": 4, |
| 309 | + "data": [ |
| 310 | + [0.0, 0.0, 0.0], |
| 311 | + [1.0, 0.0, 0.0], |
| 312 | + [1.0, 1.0, 0.0], |
| 313 | + [0.0, 1.0, 0.0], |
| 314 | + ], |
| 315 | + } |
| 316 | + ] |
| 317 | + }, |
| 318 | + } |
| 319 | + |
| 320 | + dvh_data = dvhcalc._calculate_dvh(structure, dicomparser.DicomParser(dose)) |
| 321 | + self.assertEqual(dvh_data.histogram.size, 54) |
| 322 | + # # Final bin should contain volume of single 1×1×1 mm voxel = 0.001 cm³ |
| 323 | + expected_vol = ( |
| 324 | + dose.PixelSpacing[0] |
| 325 | + * dose.PixelSpacing[1] |
| 326 | + * structure["thickness"] |
| 327 | + ) / 1000.0 |
| 328 | + self.assertAlmostEqual(dvh_data.histogram[-1], expected_vol) |
258 | 329 |
|
259 | 330 | class TestDVHCalcDecubitus(unittest.TestCase):
|
260 | 331 | """Unit tests for DVH calculation in decubitus orientations."""
|
|
0 commit comments