Getting Started with Portal Dosimetry Scripting Part II: Visualizing the Gamma Analysis
- mschmidt33
- Dec 21, 2023
- 8 min read
In a previous blog post, we discovered the prerequisite code setup for generating an application that requires access to the Portal Dosimetry Scripting API (PDSAPI). Here, we continue that same application to evaluate and visualize a gamma analysis using an example quality assurance (QA) case. The code contained in this post is lengthy, as it represents a working application in its entirety. It may be more beneficial to download the code from the following repository, and follow along with this blog post.
In the last blog post, it was shown that the AssemblyPath attribute must be added to the App.Config file in order to perform Portal Dosimetry operations. The App.Config file now contains parameters that will control the gamma parameters within this application. This will make the parameters easier to modify after the code has been compiled. The full App.Config file now looks as follows:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
</startup>
<appSettings>
<add key="AssemblyPath"
value="C:\Program Files (x86)\Varian\ProductLine\Workspaces\VMS.PortalDosimetry.Workspace\"/>
<add key="DoseDifference" value="0.02"/>
<add key="DistanceToAgreement" value="2"/>
<add key="ToleranceLevel" value="0.95"/>
</appSettings>
</configuration>In starting this application, an Application_Startup method was created to launch the application. To instantiate the upcoming ViewModels, the Patient Id and Plan Id can be extracted from the StartupEventArgs. The _patientId will be used to open the patient from the application and the _planId will be used to identify and bring the current PDPlanSetup into memory. Finally, the created View and ViewModel pair will be instantiated and the PDPlanSetup will be injected into the ViewModel. The final startup code will look as follows:
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private string _patientId;
private string _pdPlanId;
private void Application_Startup(object sender, StartupEventArgs e)
{
try
{
using (pdsapi.Application pdApp = pdsapi.Application.CreateApplication())
{ VMS.DV.PD.UI.Base.VTransientImageDataMgr.CreateInstance(true);
if (e.Args.Any())
{
_patientId = e.Args.First().Split(';').First();
_pdPlanId = e.Args.First().Split(';').Last();
}
else
{
MessageBox.Show("Missing input arguments");
this.Shutdown();
}
Patient patient = pdApp.OpenPatientById(_patientId);//check in case null patient.
PDPlanSetup plan = patient.PDPlanSetups.FirstOrDefault(ps => ps.Id.Equals(_pdPlanId));//check null case for plan
MainView mainView = new MainView();
mainView.DataContext = new MainViewModel(plan);
mainView.ShowDialog();
}
}
catch(Exception ex)
{
//TODO: output exception to user or logs.
}
}Building The View
Fields within a Portal Dosimetry plan can have any number of associated images. To organize images acquired at varying times, Portal Dosimetry associates delivered images to a Session, where the session is time-stamped to a collective acquisition of images.
Prior to building the View in Portal Dosimetry, a NUGET package will need to be added for the visualization of the resultant image. For this example, OxyPlot.WPF will be used to display the image on the WPF UI. Oxyplot is a popular plotting library with various plot types and easily integrated with consumable data-sources like ESAPI.

In the header of the MainView Window, another XML namespace can be added into the attributes to utilize oxyplot.
xmlns:oxy="http://oxyplot.org/wpf"Fields within a Portal Dosimetry plan can have any number of associated images. To organize images acquired at varying times, Portal Dosimetry associates delivered images to a Session, where the session is time-stamped to a collective acquisition of images. The view design will possess a ListView selector for the current session. To save some unnecessary coding, we will trigger the calculation of the gamma analysis based on the selection of the session -- SelectedSession referenced in the binding expression below.
<DockPanel>
<StackPanel DockPanel.Dock="Left" Margin="20">
<TextBlock Text="Sessions" FontWeight="Bold" HorizontalAlignment="Center"/>
<ListView Width="300" ItemsSource="{Binding Sessions}" SelectedItem="{Binding SelectedSession}" DisplayMemberPath="SessionDescription">
</ListView>
</StackPanel>
<Grid DockPanel.Dock="Right" Margin="0,20,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="Gamma Results" FontWeight="Bold" HorizontalAlignment="Center"/>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding SelectedSession.SessionAnalyses}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding ImageDescription}" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding AnalysisResult}" HorizontalAlignment="Center"/>
<oxy:PlotView Height="300" Width="300" Model="{Binding GammaImage}" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</DockPanel>Adjacent to the ListBox will be an ItemsControl with the DataTempate set to a small UI component that will show the results and image of the analysis. The image will use the oxy: prefix to access a PlotView.

To accommodate the properties of the GammaResults ItemsControl source, two models has been created. A Models folder houses the FieldAnalysisModel class and the SessionItem class. The SessionItem class has a property that is a collection of FieldAnalysisModel type objects for each session image.
/// <summary>
/// Model to hold the Analysis description and Gamma Analysis Image.
/// </summary>
public class FieldAnalysisModel
{
public string ImageDescription { get; set; }
public string AnalysisResult { get; set; }
public PlotModel GammaImage { get; set; }
}/// <summary>
/// Abstraction of the Portal Dosimetry Session.
/// On the selection of a SessionItem, the SessionAnalyses collection will display the analysis results.
/// </summary>
public class SessionItem
{
public string SessionId { get; set; }
public DateTime SessionDateTime { get; set; }
public string SessionDescription { get; set; }
public ObservableCollection<FieldAnalysisModel> SessionAnalyses { get; set; }
public SessionItem()
{
SessionAnalyses = new ObservableCollection<FieldAnalysisModel>();
}
}Gamma Analysis: A Review
The Gamma Analysis was originally developed as an algorithmic approach to comparing 1-Dimensional dose profiles while taking into account the dose difference between two profiles at any given point and the distance between the two profiles to where the doses will agree, simultaneously. Since then, the Gamma Analysis design has been extended into 2-D and 3-D implementations, with considerations taken into account for speed and clinical significance.
The basic premise of the gamma analysis is that each point along a reference curve (or each pixel in a 2D image) is compared to multiple points along a compared curve (or an area in a 2D image). For each comparison point, the following equation is applied (5), and the minimum value of these calculations is considered to be the gamma value (4) for that point in the reference curve.

A visualization of the application of a Gamma Analysis can be seen below:

Portal dosimetry implements this same Gamma Analysis in evaluation of the treatment beam. Within the Portal Dosimetry application, there are many parameters and evaluation tests the user can employ to investigate the quality of the beam. The tests that are available within Portal Dosimetry are described below (from the Portal Dosimetry Scripting API Reference Guide on MyVarian.com):

A [gamma]-index distribution can be generated and displayed, providing a quantitative assessment of the quality of the calculation, both in the regions that pass and fail the acceptance criteria.
ViewModel Design
The ViewModel has 2 primary purposes. Allow the user to select a SessionItem, named SelectedSession, and on the selection of the SelectedSession, calculate the SessionAnalyses. In the following code sample, The MainViewModel has 3 properties, the SelectedSession, the Sessions collection, and a private field, analyzedItems for storing the session analyses to keep the application from calculating the gamma image multiple times. On the change in the SelectedSession, the a method called SetAnalysisItems is called that will be described with the next code block.
/// <summary>
/// ViewModel for Sessions and Field Analysis Display
/// </summary>
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string v)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(v));
}
}
private PDPlanSetup _pdPlan;
private List<SessionItem> analyzedItems { get; set; }
public List<SessionItem> Sessions { get; private set; }
private SessionItem _selectedSession;
public SessionItem SelectedSession
{
get { return _selectedSession; }
set
{
_selectedSession = value;
OnPropertyChanged("SelectedSession");
SetSessionAnalysisItems();
}
}
//Constructor
public MainViewModel(PDPlanSetup plan)
{
_pdPlan = plan;
analyzedItems = new List<SessionItem>();
Sessions = new List<SessionItem>();
SetSessions();
PDCalculationService.SetAnalysisTemplate();
}Within the constructor, the Portal Dosimetry plan is extracted from the PDSAPI application that was sent from the App.Xaml.cs file. During the SetSessions method, the sessions are extracted from the _pdPlan object. Finally, the Portal Analysis Template is set from another class, called PDCalculationService described below.
/// <summary>
/// Extract measured sessions from the current PDPlan
/// </summary>
private void SetSessions()
{
foreach(var session in _pdPlan.Sessions)
{
Sessions.Add(new SessionItem
{
SessionDateTime = session.SessionDate,
SessionId = session.Id,
SessionDescription = $"{session.Id} - {session.SessionDate}"
});
}
}
/// <summary>
/// Calculate the SessionAnalyses.
/// 1. If the session has alredy been calculated, the analysis should be set to the analysis stored in memory.
/// 2. If this is a new analysis, use the PDCalculationService to calculate the analysis.
/// </summary>
private void SetSessionAnalysisItems()
{
SelectedSession.SessionAnalyses.Clear();
if (analyzedItems.Any(ai=>ai.SessionDescription == SelectedSession.SessionDescription))
{
foreach(var analysis in analyzedItems.FirstOrDefault(ai=>ai.SessionDescription == SelectedSession.SessionDescription).SessionAnalyses)
{
SelectedSession.SessionAnalyses.Add(analysis);
}
}
else
{
var session = _pdPlan.Sessions.First(s => s.Id == SelectedSession.SessionId);
foreach(var image in session.PortalDoseImages)
{ SelectedSession.SessionAnalyses.Add(PDCalculationService.PerformAnalysis(image, image.PDBeam.PredictedDoseImage));
}
}
}
}When the Session analysis items are set in the method above, one of two scenarios may play out. If the session has already been analyzed, the analysis will be pulled from the analyzedItems collection, but if the session has not been analyzed the PDCalculationService will analyze the session.
The Portal Dosimetry Services
Within the Portal Dosimetry application, there are many parameters and evaluation tests the user can employ to investigate the quality of the beam.
The PortalDosimetryService has two methods. The first method is to set up the Portal Analysis Template. There are many parameters to setting up the PDTemplate object. In this implementation, the Dose Difference and Distance-To-Agreement criteria are being extracted from the application configuration file, but all other parameters are being set directly in the code.
public static class PDCalculationService
{
private static PDTemplate _pdTemplate;
/// <summary>
/// Set PDTemplate parameters from the App.Config
/// Other parameters are hard-coded, i.e. alignment, normalization, and ROI.
/// </summary>
public static void SetAnalysisTemplate()
{
double doseDifference = Convert.ToDouble(ConfigurationManager.AppSettings["DoseDifference"]);
double distanceToAgreement = Convert.ToDouble(ConfigurationManager.AppSettings["DistanceToAgreement"]);
_pdTemplate = new PDTemplate(
false, false, false, false,
AnalysisMode.CU,
NormalizationMethod.MaxPredictedDose,
true, 0.05, ROIType.None,
10,
doseDifference,
distanceToAgreement,
false,
new List<EvaluationTestDesc>
{
new EvaluationTestDesc(EvaluationTestKind.GammaAreaLessThanOne,
0.0, Convert.ToDouble(ConfigurationManager.AppSettings["Tolerance"]),
true)
});
}For a review of all parameters in the PDTemplate constructor, it may be helpful to compare each parameter to the equivalent selection in the Portal Dosimetry workspace. Finally, the EvaluationTestDesc class allows the user to test the gamma evaluation for passing/failing against a number of criteria.


Finally, the analysis is applied for each pair of images selected. The analysis result is extracted from the CreateTransientAnalysis method with the PDTemplate applied. The GammaImage is then accessed from the analysis and used in the GetGammaImage method to convert the image to an oxyplot HeatmapSeries. The key here, is to extract the pixel values of the GammaImage using the GetVoxels method in the Portal Dosimetry API.
/// <summary>
/// Perform analysis
/// </summary>
/// <param name="portalImage">Measured Portal Dosimetry image</param>
/// <param name="predictedImage">Predicted Portal Dosimetry Image</param>
/// <returns>FieldAnalysisModel</returns>
public static FieldAnalysisModel PerformAnalysis(PortalDoseImage portalImage, DoseImage predictedImage)
{
FieldAnalysisModel fieldModel = new FieldAnalysisModel();
fieldModel.ImageDescription = $"Beam: {portalImage.PDBeam.Id}, Image: {portalImage.Id}\nDate: {portalImage.HistoryDateTime}";
var analysis = portalImage.CreateTransientAnalysis(_pdTemplate, predictedImage);
fieldModel.AnalysisResult =$"Gamma Analysis [{_pdTemplate.GammaParamDoseDiff*100.0}%/{_pdTemplate.GammaParamDTA}mm] = {analysis.EvaluationTests.First().TestValue*100.0:F2}%";
fieldModel.GammaImage = GetGammaImage(analysis.GammaImage);
return fieldModel;
}
/// <summary>
/// Converts Portal Dosimetry API image into an OxyPlot PlotView
/// </summary>
/// <param name="gammaImage">Gamma Image from Portal Dosimetry Analysis</param>
/// <returns>Plot View (OxyPlot.WPF)</returns>
private static PlotModel GetGammaImage(ImageRT gammaImage)
{
var gImage = gammaImage.FramesRT.First();
ushort[,] pixels = new ushort[gImage.XSize, gImage.YSize];
double[,] plotPixels = new double[gImage.XSize, gImage.YSize];
gammaImage.FramesRT.First().GetVoxels(0, pixels);
for (int i = 0; i < gImage.XSize; i++)
{
for (int j = 0; j < gImage.YSize; j++)
{
double value = gImage.VoxelToDisplayValue(pixels[i, j]);
plotPixels[i, gImage.YSize - 1 - j] = value < 1 ? value : value * 100.0;
}
}
PlotModel plt = new PlotModel();
plt.Axes.Add(new LinearColorAxis
{
Palette = OxyPalettes.Plasma(100),
IsAxisVisible = false
});
HeatMapSeries hms = new HeatMapSeries()
{
X0 = 0,
X1 = gImage.XSize,
Y0 = 0,
Y1 = gImage.YSize,
RenderMethod = HeatMapRenderMethod.Bitmap,
Data = plotPixels,
};
plt.Series.Add(hms);
return plt;
}
}There is one modification to the gamma image map that is being made that is worth distinction here. The following lines of code are pulling the gamma analysis value from the image and applying them to an array to later be used in the Heatmap series. The second line is checking if the plot value is < 1.0, and if so, using the gamma value, but if not multiplying the gamma value by a factor of 100.0.
double value = gImage.VoxelToDisplayValue(pixels[i, j]);
plotPixels[i, gImage.YSize - 1 - j] = value < 1 ? value : value * 100.0;Within the Portal Dosimetry application it is typical to see an abrupt change in the pixel colors between passing and failing values within the gamma image. Without this factor, the gamma image will be a linear scale between the maximum and minimum value, causing the image to look as though there are many failures.

Applications of this Technology
Prior to running the application in Visual Studio, the input arguments for the _patientId and _pdPlan must be injected into the Application_Startup method as StartupEventArgs. The arguments are expected to be in a semi-colon delimited string. This string can be entered into Visual Studio by going to the Project dropdown and selecting SamplePDSAPI Properties. In the Debug tab, the Command Line Arguments can be entered where PatientID and PlanID are set to the current patient and plan to be used for testing.

When testing this application, it is possible to change the gamma analysis parameters between runs and verify that the calculations are matching Portal Dosimetry in both gamma analysis result and gamma image visualization.

It may also be useful to explore various Heatmap types in oxyplot. Below are some examples, but there are more available in the API.

PDSAPI is a convenient technology that could be useful in the development of scripts to assist in the analysis of Portal Dosimetry images for patient-specific QA, linear accelerator QA, commissioning, and report building.







Comments