├── .gitignore ├── README.md ├── dta ├── __init__.py └── algorithms.py ├── example.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | 4 | dist/ 5 | build/ 6 | pygamma.egg-info/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Distance to agreement calculations for radiotherapy dose distributions 2 | ====================================================================== 3 | 4 | RESEARCH USE ONLY 5 | 6 | Currently an n-dimensional version of gamma evaluation is the only algorithm implemented. The resolution in the sample and reference cannot be different, however each axis of sample and reference respectively can have a different resolution. 7 | 8 | from dta import gamma_evaluation 9 | 10 | distance = 3 # 3 mm 11 | threshold = reference.max()*0.03 # 3 % of max in reference 12 | sample_res, reference_res = (2, 2) # 2 mm voxels in sample, 2 mm in ref 13 | 14 | gamma_map = gamma_evaluation(sample, reference, 15 | distance, threshold, 16 | (sample_res, reference_res)) 17 | 18 | ## Installation 19 | 20 | git clone https://github.com/christopherpoole/pygamma.git 21 | # OR download the repository as a zip file 22 | cd pygamma 23 | python setup.py install 24 | 25 | ## Signed Gamma Evaluation 26 | Signed gamma evaluation makes hot and cold spots obvious in the calculated gamma map, to use it: 27 | 28 | gamma_evaluation(sample, reference, distance, threshold, (sam_res, ref_res), signed=True) 29 | 30 | See here for more details: 31 | 32 | @article{mohammadi2012modification, 33 | title={Modification of the gamma function for the recognition of over-and under-dose regions in three dimensions}, 34 | author={Mohammadi, Mohammad and Rostampour, Nima and Rutten, Thomas P}, 35 | journal={Journal of medical physics/Association of Medical Physicists of India}, 36 | volume={37}, 37 | number={4}, 38 | pages={200}, 39 | year={2012}, 40 | publisher={Medknow Publications} 41 | } 42 | -------------------------------------------------------------------------------- /dta/__init__.py: -------------------------------------------------------------------------------- 1 | from algorithms import gamma_evaluation 2 | -------------------------------------------------------------------------------- /dta/algorithms.py: -------------------------------------------------------------------------------- 1 | # License & Copyright 2 | # =================== 3 | # 4 | # Copyright 2012 Christopher M Poole 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | # 19 | # 20 | # Distance to Agreement using Gamma Evaluation 21 | # 22 | # Author: Christopher M Poole 23 | # Email: mail@christopherpoole.net 24 | # Date: 20st April, 2012 25 | 26 | 27 | import numpy 28 | 29 | from math import ceil 30 | from scipy.ndimage.filters import generic_filter 31 | 32 | 33 | def gamma_evaluation(sample, reference, distance, threshold, resolution, signed=False): 34 | """ 35 | Distance to Agreement between a sample and reference using gamma evaluation. 36 | 37 | Parameters 38 | ---------- 39 | sample : ndarray 40 | Sample dataset, simulation output for example 41 | reference : ndarray 42 | Reference dataset, what the `sample` dataset is expected to be 43 | distance : int 44 | Search window limit in the same units as `resolution` 45 | threshold : float 46 | The maximum passable deviation in `sample` and `reference` 47 | resolution : tuple 48 | The resolution of each axis of `sample` and `reference` 49 | signed : bool 50 | Returns signed gamma for identifying hot/cold fails 51 | 52 | Returns 53 | ------- 54 | gamma_map : ndarray 55 | g == 0 (pass) the sample and reference pixels are equal 56 | 0 < g <= 1 (pass) agreement within distance and threshold 57 | g > 1 (fail) no agreement 58 | """ 59 | 60 | ndim = len(resolution) 61 | assert sample.ndim == reference.ndim == ndim, \ 62 | "`sample` and `reference` dimensions must equal `resolution` length" 63 | assert sample.shape == reference.shape, \ 64 | "`sample` and `reference` must have the same shape" 65 | 66 | # First we need to construct the distance penalty kernel, for this we use 67 | # a meshgrid. The trick is creating the appropriate slices. 68 | 69 | # We require one slice per dimension and around the current point 70 | # between -distance/resolution and +distance/resolution. We transpose (.T) 71 | # so we have a colum vector. 72 | resolution = numpy.array(resolution)[[numpy.newaxis for i in range(ndim)]].T 73 | slices = [slice(-ceil(distance/r), ceil(distance/r)+1) for r in resolution] 74 | 75 | # Scale the meshgide to the resolution of the images, google "numpy.mgrid" 76 | kernel = numpy.mgrid[slices] * resolution 77 | 78 | # Distance squared from the central voxel 79 | kernel = numpy.sum(kernel**2, axis=0) 80 | 81 | # If the distance from the central voxel is greater than the distance 82 | # threshold, set to infinity so that it will not be selected as the minimum 83 | kernel[numpy.where(numpy.sqrt(kernel) > distance)] = numpy.inf 84 | 85 | # Divide by the square of the distance threshold, this is the cost penalty 86 | kernel = kernel / distance**2 87 | 88 | # ndimag.generic_filter needs to know the footprint of the 89 | # kernel which must be flat 90 | footprint = numpy.ones_like(kernel) 91 | kernel = kernel.flatten() 92 | 93 | # Apply the dose threshold penalty (see gamma evaluation equation), here 94 | # we are still under the sqrt. 95 | values = (reference - sample)**2 / (threshold)**2 96 | 97 | # Move the distance penalty kernel over the dose penalised values and search 98 | # for the minimum of the sum between the kernel and the values under it. This 99 | # is the point of closest agreement. 100 | gamma_map = generic_filter(values, \ 101 | lambda vals: numpy.minimum.reduce(vals + kernel), footprint=footprint) 102 | 103 | # Euclidean distance 104 | gamma_map = numpy.sqrt(gamma_map) 105 | 106 | # Rough signed gamma evaluation. 107 | if (signed): 108 | return gamma_map * numpy.sign(sample - reference) 109 | else: 110 | return gamma_map 111 | 112 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import pylab 3 | 4 | from dta import gamma_evaluation 5 | 6 | # Reference data with (2, 1) mm resolution 7 | reference = numpy.random.random((128, 256)) 8 | #reference = numpy.abs(reference) 9 | reference /= reference.max() 10 | reference *= 100 11 | reference -= 50 12 | 13 | # Sample data with a %3 shift on the reference 14 | sample = reference * 1.03 15 | 16 | # Perform gamma evaluation at 4mm, 2%, resoution x=2, y=1 17 | gamma_map = gamma_evaluation(sample, reference, 4., 2., (2, 1), signed=True) 18 | 19 | pylab.imshow(gamma_map, cmap='RdBu_r', aspect=2, vmin=-2, vmax=2) 20 | pylab.colorbar() 21 | pylab.show() 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = "pygamma", 5 | version = "0.1", 6 | author = "Christopher Poole", 7 | author_email = "mail@christopherpoole.net", 8 | description = "Perform a gamma evaluation comparing 2 n-dim radiotherapy dose distributions", 9 | keywords = "radiotherapy, gamma evaluation, distance to agreement", 10 | url = "http://github.com/christopherpoole/pygamma", 11 | packages = ["dta"], 12 | long_description=open("README.md").read(), 13 | ) 14 | --------------------------------------------------------------------------------