8.3 小体積輪郭のチェック

目的

ROIの輪郭に小さい面積のものが含まれていないかを調べる。

必要な情報

プランで用いられている画像、 調べたいROIのStructureオブジェクト、 ROIの輪郭の座標点

与えられている引数

PlanSetupクラスのインスタンスplanSetup

必要な情報へのアクセス方法

  • IDがBODYStructureオブジェクトの取得
    1
    2
    3
    4
    5
    6
    7
    8
    string structureName = "BODY";
    var query = planSetup.StructureSet.Structures.Where(s => s.Id == structureName);
    if (query.Count() != 1)
    {
        MessageBox.Show(String.Format("No structure: {0}", structureName));
        return String.Format("No structure: {0}\n", structureName);
    }
    var structure = query.Single();
    
  • 現在開かれているプラン(planSetup)で用いられている画像の取得
    1
    var image = planSetup.StructureSet.Image;
    
  • スライスiにおけるsturctureの輪郭(VVector[][])の取得
    1
    contours = structure.GetContoursOnImagePlane(i)
    
    contoursVVector[]の配列。VVector[]が一つの輪郭に対応。 1スライスに複数の輪郭がある可能性もある。 一つ一つのVVectorが輪郭の点に対応。

詳しくは 6.3 リファレンスポイントと体表面のチェック を参照。

8_3_1 8_3_2 n点からなる多角形の面積を求める

必要な情報の表示

実装

  • 体輪郭(BODY)に小さい面積のものが含まれていないかを確認し、結果を返す関数
    1
    2
    public static string CheckSmallSegments(PlanSetup planSetup)
    {
    
  • デフォルトの許容する最小の面積(cm2)の指定
    1
    2
    3
    4
        double defaultMinimumSegmentAreaCm2 = 0.5;
    
        // Threshold area in mm2
        double thresholdArea;
    
  • doesAskThreholdAreatrueの場合、最小面積の許容値thresholdArea(mm)の入力を求めるタイアログを表示する
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
        //If doesAskThreholdArea is true, input window for minimum segment area pops up
        bool doesAskThreholdArea = true;
        if (!doesAskThreholdArea)
        {
            thresholdArea = defaultMinimumSegmentAreaCm2 * 100;
        }
        else
        {
            var inputWindow = new InputWindow("Minimum segment area", "Minimum segment area in cm2", defaultMinimumSegmentAreaCm2.ToString("0.00"));
            inputWindow.Window.ShowDialog();
    
            if ((!inputWindow.IsOk) || string.IsNullOrWhiteSpace(inputWindow.InputText))
            {
                return "Small segment area check is canceled\n";
            }
    
            double thresholdAreaCm2;
            if (double.TryParse(inputWindow.InputText, out thresholdAreaCm2))
            {
                // cm2 to mm2
                thresholdArea = thresholdAreaCm2 * 100;
            }
            else
            {
                return string.Format("Invalid input for segment area: {0}\n", inputWindow.InputText);
            }
        }
    
  • IDがBODYStructureオブジェクトを取得
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
        string structureName = "BODY";
        var query = planSetup.StructureSet.Structures.Where(s => s.Id == structureName);
        if (query.Count() != 1)
        {
            MessageBox.Show(String.Format("No structure: {0}", structureName));
            return String.Format("No structure: {0}\n", structureName);
        }
        var structure = query.Single();
    
        //MessageBox.Show(String.Format("{0}", structure.GetNumberOfSeparateParts()));
    
  • プランで用いられている画像の取得
    1
        var image = planSetup.StructureSet.Image;
    
  • 許容値以下の面積の輪郭の重心位置を格納する配列の定義
    1
        var smallAreas = new List<double[]>();
    
  • 全てのスライスを巡るループ
    1
    2
    3
        for (int i = 0; i < image.ZSize; i++)
        {
            var z = image.Origin.z + i * image.ZRes;
    
  • スライスi上の輪郭を取得(複数ある場合もある)
    1
    2
    3
    4
    5
    6
            var contours = structure.GetContoursOnImagePlane(i);
            if (contours.Length > 0)
            {
                //Console.WriteLine("z index: {0}, Number of contours: {1}", i, contours.Length);
                for (int j = 0; j < contours.Length; j++)
                {
    
  • 輪郭の面積(area)を、輪郭を多角形として求める補助関数
    1
    2
                    var area = AreaOfPolygon(contours[j]);
                    //Console.WriteLine("\tindex: {0}, Area: {1}", j, area);
    
  • 輪郭の面積(area)が許容値(thresholdArea)以下であれば、重心座標を求め、それを配列(smallAreas)に格納する。
    1
    2
    3
    4
    5
    6
    7
    8
    9
                    if (area <= thresholdArea)
                    {
                        var centerOfMass = CenterOfMassOfPolygon(contours[j]);
                        var smallArea = new double[] { centerOfMass[0], centerOfMass[1], centerOfMass[2], area };
                        smallAreas.Add(smallArea);
                    }
                }
            }
        }
    
  • 許容値以下の面積の輪郭がない場合の結果の書き出し
    1
    2
    3
    4
    5
        if (smallAreas.Count == 0)
        {
            string oText = string.Format("No small segment less than {0} cm2 in {1}", thresholdArea * 0.01, structureName);
            return MakeFormatText(true, "Check small segments", oText);
        }
    
  • 許容値以下の面積の輪郭の重心座標の書き出し
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
        string smallAreaResult = string.Format("Small segments (<= {0} cm2 ) in {1}:\n", thresholdArea * 0.01, structureName);
        foreach (var smallArea in smallAreas)
        {
            var x = smallArea[0];
            var y = smallArea[1];
            var z = smallArea[2];
            var area = smallArea[3];
    
            var centerOfMassUcs = image.DicomToUser(new VVector(x, y, z), planSetup);
    
            string result = String.Format("\n({0:0.00}, {1:0.00}, {2:0.00}): {3:0.000} cm2\n",
                centerOfMassUcs.x / 10, centerOfMassUcs.y / 10, centerOfMassUcs.z / 10, area * 0.01);
            smallAreaResult += result;
        }
    
        return MakeFormatText(false, "Check small segments", smallAreaResult);
    }
    
  • 多角形の頂点(VVector[] points)を入力として受け取り、その面積を返す関数
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /// <summary>
    /// Area of polygon
    /// Reference : https://imagingsolution.net/math/calc_n_point_area/
    /// </summary>
    /// <remarks>
    /// All points are assumed in the same z plane 
    /// </remarks>
    /// <param name="points"> Array of VVectors of vertices of polygon </param>
    /// <returns> Area of polygon </returns>
    public static double AreaOfPolygon(VVector[] points)
    {
    
        int numberOfPoints = points.Length;
    
        // a point or line 
        if (numberOfPoints < 3)
        {
            return 0;
        }
    
        // Calculating area using outer product
        double sum = 0;
        for (int i = 0; i < numberOfPoints - 1; i++)
        {
            sum += points[i].x * points[i + 1].y - points[i].y * points[i + 1].x;
        }
    
        sum += points[numberOfPoints - 1].x * points[0].y - points[numberOfPoints - 1].y * points[0].x;
    
        return 0.5 * Math.Abs(sum);
    }
    
  • 点の配列(VVector[] points)を入力として、その重心位置を返す関数
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /// <summary>
    /// Center of mass coordinate in the z plane
    /// </summary>
    /// <remarks>
    /// z coordinates are ignored.
    /// </remarks>
    /// <param name="points"> Array of VVector of the point coordinates </param>
    /// <returns> Center of mass coordinate </returns>
    public static double[] CenterOfMassOfPolygon(VVector[] points)
    {
        int numberOfPoints = points.Length;
    
        double xSum = 0;
        double ySum = 0;
    
        for (int i = 0; i < numberOfPoints; i++)
        {
            xSum += points[i].x;
            ySum += points[i].y;
        }
    
        return new double[] { xSum / numberOfPoints, ySum / numberOfPoints, points[0].z };
    }
    
  • テキストの入力を求めるダイアログを開く補助クラス  
    • 入力値を実行時に切り替えたい場合もあると思うので加えてみた。
    • ComboBoxの部分がコメント・アウトされているが、それを戻せば複数の選択肢から一つ選ぶコンボボックスをダイアログに含めることも可能。
    • Visual Studio(VS)が使えない場合を想定して、GUIの部分は直接コードに書き込んでいるが、VSが使えるのであれば、VSでXAMLを用いて書いた方が簡単。
    • 入力する値を切り替えたい場合はテキストファイルを読み込むことでも対応可能。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/// <summary>
/// Window for Input using TextBox and ComboBox
/// Reference: https://stackoverflow.com/questions/8103743/wpf-c-sharp-inputbox
/// </summary>
public class InputWindow
{
    public Window Window;
    public bool IsOk = false;
    public string WindowTitle;

    public string InputText;
    public string InputBoxTitle;
    public string DefaultInputBoxValue;
    private TextBox InputBox;

    //public string SelectedItem;
    //public string ComboBoxTitle;
    //private ComboBox ComboBox;

    public InputWindow(string windowTitle, string inputBoxTitle, string defaultInputBoxValue)
    {
        WindowTitle = windowTitle;
        InputBoxTitle = inputBoxTitle;
        DefaultInputBoxValue = defaultInputBoxValue;

        Window = new Window
        {
            SizeToContent = SizeToContent.WidthAndHeight,
            Title = WindowTitle,
            WindowStartupLocation = WindowStartupLocation.CenterScreen
        };

        var InputBoxLabel = new Label();
        InputBoxLabel.Content = InputBoxTitle;

        InputBox = new TextBox();
        InputBox.MinWidth = 120;

        if (!string.IsNullOrEmpty(DefaultInputBoxValue))
        {
            InputBox.Text = DefaultInputBoxValue;
        }

        //var ComboBoxLabel = new Label();
        //ComboBoxLabel.Content = ComboBoxLabel;
        //ComboBox = new ComboBox();
        //ComboBox.ItemsSource = new List<string> { "PTV", "CTV" };
        //ComboBox.MinWidth = 120;

        var OkButton = new Button
        {
            Content = "OK",
            Margin = new Thickness(3),
            Width = 72
        };

        OkButton.Click += OkButton_Click;

        var CancelButton = new Button
        {
            Content = "Cancel",
            Margin = new Thickness(3),
            Width = 72
        };

        CancelButton.Click += CancelButton_Click;

        var stackPanelForButtons = new StackPanel();
        stackPanelForButtons.Orientation = Orientation.Horizontal;
        stackPanelForButtons.Children.Add(OkButton);
        stackPanelForButtons.Children.Add(CancelButton);

        var stackPanel = new StackPanel();
        Window.Content = stackPanel;

        stackPanel.Children.Add(InputBoxLabel);
        stackPanel.Children.Add(InputBox);
        //stackPanel.Children.Add(ComboBox);
        stackPanel.Children.Add(stackPanelForButtons);
    }

    private void OkButton_Click(object sender, RoutedEventArgs e)
    {
        IsOk = true;

        //SelectedItem = ComboBox.SelectedItem.ToString();
        InputText = InputBox.Text;

        Window.Close();
    }

    private void CancelButton_Click(object sender, RoutedEventArgs e)
    {
        Window.Close();
    }
}