Add some GUI elements :
authorGreg Burri <greg.burri@gmail.com>
Fri, 22 Jan 2016 11:38:55 +0000 (12:38 +0100)
committerGreg Burri <greg.burri@gmail.com>
Fri, 22 Jan 2016 11:38:55 +0000 (12:38 +0100)
- A warning if not enough erytrocytes
- The list of manually modifier erytrocytes
- Image names
- Exportation des résultats

13 files changed:
Parasitemia/ParasitemiaCore/Ellipse.fs
Parasitemia/ParasitemiaCore/ImgTools.fs
Parasitemia/ParasitemiaCore/MainAnalysis.fs
Parasitemia/ParasitemiaCore/MatchingEllipses.fs
Parasitemia/ParasitemiaUI/Export.fs [new file with mode: 0644]
Parasitemia/ParasitemiaUI/GUI.fs
Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj
Parasitemia/ParasitemiaUI/PiaZ.fs
Parasitemia/ParasitemiaUI/Program.fs
Parasitemia/ParasitemiaUI/State.fs
Parasitemia/ParasitemiaUI/Types.fs
Parasitemia/ParasitemiaUI/Utils.fs [new file with mode: 0644]
Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml

index e65100b..1059e3b 100644 (file)
@@ -261,8 +261,8 @@ let private areVectorsValid (p1x: float32) (p1y: float32) (p2x: float32) (p2y: f
 
 
 let find (edges: Matrix<byte>)
-         (xGradient: Image<Gray, float32>)
-         (yGradient: Image<Gray, float32>)
+         (xGradient: Matrix<float32>)
+         (yGradient: Matrix<float32>)
          (config: Config) : MatchingEllipses =
 
     let r1, r2 = config.RBCRadius.Min, config.RBCRadius.Max
@@ -334,7 +334,7 @@ let find (edges: Matrix<byte>)
                            squaredDistance p1xf p1yf p3xf p3yf >= squaredMinimumDistance &&
                            squaredDistance p2xf p2yf p3xf p3yf >= squaredMinimumDistance
                         then
-                            match areVectorsValid (float32 p1xf) (float32 p1yf) (float32 p2xf) (float32 p2yf) -xDirData.[p1.Y, p1.X, 0] -yDirData.[p1.Y, p1.X, 0] -xDirData.[p2.Y, p2.X, 0] -yDirData.[p2.Y, p2.X, 0] with
+                            match areVectorsValid (float32 p1xf) (float32 p1yf) (float32 p2xf) (float32 p2yf) -xDirData.[p1.Y, p1.X] -yDirData.[p1.Y, p1.X] -xDirData.[p2.Y, p2.X] -yDirData.[p2.Y, p2.X] with
                             | Some (m1, m2) ->
                                 match ellipse2 p1xf p1yf (float m1) p2xf p2yf (float m2) p3xf p3yf with
                                 | Some e when e.Cx > 0.f && e.Cx < w_f - 1.f && e.Cy > 0.f && e.Cy < h_f - 1.f &&
index 44a6c86..3fb8ecf 100644 (file)
@@ -168,31 +168,19 @@ let suppressMAdjacency (img: Matrix<byte>) =
 /// The thresholds are automatically defined with otsu on gradient magnitudes.
 /// </summary>
 /// <param name="img"></param>
-let findEdges (img: Image<Gray, float32>) : Matrix<byte> * Image<Gray, float32> * Image<Gray, float32> =
+let findEdges (img: Image<Gray, float32>) : Matrix<byte> * Matrix<float32> * Matrix<float32> =
     let w = img.Width
     let h = img.Height
 
     use sobelKernel =
-        new ConvolutionKernelF(array2D [[ 1.0f; 0.0f; -1.0f ]
-                                        [ 2.0f; 0.0f; -2.0f ]
-                                        [ 1.0f; 0.0f; -1.0f ]], Point(1, 1))
+        new Matrix<float32>(array2D [[ 1.0f; 0.0f; -1.0f ]
+                                     [ 2.0f; 0.0f; -2.0f ]
+                                     [ 1.0f; 0.0f; -1.0f ]])
 
-    let xGradient = img.Convolution(sobelKernel)
-    let yGradient = img.Convolution(sobelKernel.Transpose())
-
-    let xGradientData = xGradient.Data
-    let yGradientData = yGradient.Data
-    for r in 0 .. h - 1 do
-        xGradientData.[r, 0, 0] <- 0.f
-        xGradientData.[r, w - 1, 0] <- 0.f
-        yGradientData.[r, 0, 0] <- 0.f
-        yGradientData.[r, w - 1, 0] <- 0.f
-
-    for c in 0 .. w - 1 do
-        xGradientData.[0, c, 0] <- 0.f
-        xGradientData.[h - 1, c, 0] <- 0.f
-        yGradientData.[0, c, 0] <- 0.f
-        yGradientData.[h - 1, c, 0] <- 0.f
+    let xGradient = new Matrix<float32>(img.Size)
+    let yGradient = new Matrix<float32>(img.Size)
+    CvInvoke.Filter2D(img, xGradient, sobelKernel, Point(1, 1))
+    CvInvoke.Filter2D(img, yGradient, sobelKernel.Transpose(), Point(1, 1))
 
     use magnitudes = new Matrix<float32>(xGradient.Size)
     use angles = new Matrix<float32>(xGradient.Size)
@@ -223,8 +211,8 @@ let findEdges (img: Image<Gray, float32>) : Matrix<byte> * Image<Gray, float32>
 
     for i in 1 .. h - 2 do
         for j in 1 .. w - 2 do
-            let vx = xGradientData.[i, j, 0]
-            let vy = yGradientData.[i, j, 0]
+            let vx = xGradientData.[i, j]
+            let vy = yGradientData.[i, j]
             if vx <> 0.f || vy <> 0.f
             then
                 let angle = anglesData.[i, j]
@@ -942,7 +930,7 @@ let drawLineF (img: Image<'TColor, 'TDepth>) (color: 'TColor) (x0: float) (y0: f
 let drawEllipse (img: Image<'TColor, 'TDepth>) (e: Ellipse) (color: 'TColor) (alpha: float) =
     if alpha >= 1.0
     then
-        img.Draw(Emgu.CV.Structure.Ellipse(PointF(float32 e.Cx, float32 e.Cy), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias)
+        img.Draw(Emgu.CV.Structure.Ellipse(PointF(e.Cx, e.Cy), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias)
     else
         let windowPosX = e.Cx - e.A - 5.f
         let gapX = windowPosX - (float32 (int windowPosX))
@@ -956,7 +944,7 @@ let drawEllipse (img: Image<'TColor, 'TDepth>) (e: Ellipse) (color: 'TColor) (al
         if roi = img.ROI // We do not display ellipses touching the edges (FIXME)
         then
             use i = new Image<'TColor, 'TDepth>(img.ROI.Size)
-            i.Draw(Emgu.CV.Structure.Ellipse(PointF(float32 <| (e.A + 5.f + gapX) , float32 <| (e.A + 5.f + gapY)), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias)
+            i.Draw(Emgu.CV.Structure.Ellipse(PointF(e.A + 5.f + gapX, e.A + 5.f + gapY), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias)
             CvInvoke.AddWeighted(img, 1.0, i, alpha, 0.0, img)
         img.ROI <- Rectangle.Empty
 
index 7b8b066..4ab52d9 100644 (file)
@@ -50,7 +50,7 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
 
         logWithName "Starting analysis ..."
 
-        use green = img.Item(1)
+        use green = img.[1]
         let greenFloat = green.Convert<Gray, float32>()
         let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation
 
index c7a0627..3ed8ad6 100644 (file)
@@ -9,6 +9,12 @@ open System.Collections.Generic
 open Types
 open Utils
 
+// All ellipses with a score below this are removed.
+let matchingScoreThreshold = 0.4f
+let matchingScorePower = 20.f
+let windowSizeRadiusFactor = 1.f / 2.f
+let minimumDistanceFromCenterRadiusFactor = 1.f / 3.f
+
 type private EllipseScoreFlaggedKd (matchingScore: float32, e: Ellipse) =
     let mutable matchingScore = matchingScore
 
@@ -29,9 +35,6 @@ type private EllipseScoreFlaggedKd (matchingScore: float32, e: Ellipse) =
 type MatchingEllipses (radius: float32) =
     let ellipses = List<EllipseScoreFlaggedKd>()
 
-    // All ellipses with a score below this are removed.
-    let matchingScoreThreshold = 0.4f
-
     member this.Add (e: Ellipse) =
         ellipses.Add(EllipseScoreFlaggedKd(0.f, e))
 
@@ -47,7 +50,7 @@ type MatchingEllipses (radius: float32) =
             let tree = KdTree.Tree.BuildTree (List.ofSeq ellipses)
 
             // 2) Compute the matching score of each ellipses.
-            let windowSize = radius / 3.f
+            let windowSize = radius * windowSizeRadiusFactor
             for e in ellipses do
                 e.Processed <- true
                 let areaE = e.Ellipse.Area
@@ -63,7 +66,7 @@ type MatchingEllipses (radius: float32) =
                         | Some (overlapArea, _, _)
                             // Because of approximation error, see https://github.com/chraibi/EEOver/issues/4
                             when overlapArea - areaE < 1.f && overlapArea - areaOther < 1.f ->
-                                let matchingScore = (2.f * overlapArea / (areaE + areaOther)) ** 30.f
+                                let matchingScore = (2.f * overlapArea / (areaE + areaOther)) ** matchingScorePower
                                 other.AddMatchingScore(matchingScore)
                                 e.AddMatchingScore(matchingScore)
                         | _ -> ()
@@ -82,7 +85,7 @@ type MatchingEllipses (radius: float32) =
                         if not other.Removed && e.MatchingScore > other.MatchingScore
                         then
                             // Case where ellipses are too close.
-                            if distanceTwoPoints (PointF(e.Ellipse.Cx, e.Ellipse.Cy)) (PointF(other.Ellipse.Cx, other.Ellipse.Cy)) < 0.3f * e.Ellipse.B
+                            if distanceTwoPoints (PointF(e.Ellipse.Cx, e.Ellipse.Cy)) (PointF(other.Ellipse.Cx, other.Ellipse.Cy)) < minimumDistanceFromCenterRadiusFactor * e.Ellipse.B
                             then
                                 other.Removed <- true
                             else
diff --git a/Parasitemia/ParasitemiaUI/Export.fs b/Parasitemia/ParasitemiaUI/Export.fs
new file mode 100644 (file)
index 0000000..adecd6c
--- /dev/null
@@ -0,0 +1,24 @@
+module ParasitemiaUI.Export
+
+open System
+open System.IO
+
+open State
+
+/// <exception cref="System.IOException">If the results cannot be exported</exception>
+let exportResults (state: State) (filePath: string) =
+    use writer = new StreamWriter(new FileStream(filePath, FileMode.Create, FileAccess.Write))
+    fprintfn writer "File: %s" state.FilePath
+    fprintfn writer "Export date: %A" DateTime.Now
+
+    fprintfn writer ""
+    fprintfn writer "Patient ID: %s" state.PatientID
+    fprintfn writer "Global parasitemia: %s" (Utils.percentText state.GlobalParasitemia)
+
+    for srcImg in state.SourceImages do
+        fprintfn writer ""
+        fprintfn writer "Image name: %s" srcImg.name
+        fprintfn writer "Parasitemia: %s" (Utils.percentText (state.ImageParasitemia srcImg))
+        fprintfn writer "Added infected erythrocyte: %s %s" (state.ImageNbManuallyChangedRBCStr srcImg true) (state.ImageManuallyChangedRBCStr srcImg true)
+        fprintfn writer "Removed infected erythrocyte: %s %s" (state.ImageNbManuallyChangedRBCStr srcImg false) (state.ImageManuallyChangedRBCStr srcImg false)
+    ()
\ No newline at end of file
index 2ff3116..d749479 100644 (file)
@@ -27,12 +27,14 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let state = State.State()
     let mutable currentScale = 1.
     let mutable displayHealthy = false
+    let warningBelowNumberOfRBC = 1000
 
     let menuExit: MenuItem = ctrl "menuExit"
     let menuSaveFile: MenuItem = ctrl "menuSave"
     let menuSaveAsFile: MenuItem = ctrl "menuSaveAs"
     let menuLoadFile: MenuItem = ctrl "menuOpen"
     let menuNewFile: MenuItem = ctrl "menuNew"
+    let menuExportResults: MenuItem = ctrl "menuExportResults"
     let menuAddSourceImage: MenuItem = ctrl "menuAddSourceImage"
     let menuAnalysis: MenuItem = ctrl "menuAnalysis"
     let menuStartAnalysis: MenuItem = ctrl "menuStartAnalysis"
@@ -44,15 +46,18 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let txtMessageStatus: TextBlock = ctrl "txtMessageStatus"
 
     let txtPatient: TextBox = ctrl "txtPatient"
-    let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia"
+    let txtGlobalParasitemia: TextBlock = ctrl "txtGlobalParasitemia"
 
     let stackPreviews: StackPanel = ctrl "stackPreviews"
 
     let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage"
     let borderCurrentImage: Border = ctrl "borderCurrentImage"
     let canvasCurrentImage: Canvas = ctrl "canvasCurrentImage"
+
+    let gridImageInformation: Grid = ctrl "gridImageInformation"
     let txtImageInformation1: TextBlock = ctrl "txtImageInformation1"
     let txtImageInformation2: TextBlock = ctrl "txtImageInformation2"
+    let txtImageName: TextBox = ctrl "txtImageName"
 
     let scrollRBC: ScrollViewer = ctrl "scrollRBC"
     let stackRBC: StackPanel = ctrl "stackRBC"
@@ -117,21 +122,24 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
         scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
 
-    let percentText (nbTotal: int, nb: int) : string =
-        if nbTotal = 0
-        then
-            ""
-        else
-            let percent = 100. * (float nb) / (float nbTotal)
-            sprintf "%.1f %% (%d / %d)" percent nb nbTotal
+
+    let txtImageName_TextChanged =
+        TextChangedEventHandler(fun obj args -> state.CurrentImage |> Option.iter(fun srcImg -> state.SetName srcImg txtImageName.Text))
 
     let updateCurrentImageInformation () =
+        txtImageName.TextChanged.RemoveHandler(txtImageName_TextChanged)
         txtImageInformation1.Inlines.Clear()
         txtImageInformation2.Inlines.Clear()
+        txtImageName.Text <- ""
 
         match state.CurrentImage with
         | Some srcImg ->
-            let parasitemiaStr = percentText (state.ImageParasitemia srcImg)
+            gridImageInformation.Visibility <- Visibility.Visible
+            txtImageName.Text <- srcImg.name
+            txtImageName.TextChanged.AddHandler(txtImageName_TextChanged)
+
+            // The left part.
+            let parasitemiaStr = Utils.percentText (state.ImageParasitemia srcImg)
             txtImageInformation1.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
             txtImageInformation1.Inlines.Add(parasitemiaStr)
             txtImageInformation1.Inlines.Add(Documents.LineBreak())
@@ -139,17 +147,27 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             txtImageInformation1.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
             txtImageInformation1.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
 
-            let alteredStr = percentText (state.ImageNbAltered srcImg)
-            txtImageInformation2.Inlines.Add(Documents.Run("Number of erytrocytes manually altered: ", FontWeight = FontWeights.Bold))
-            txtImageInformation2.Inlines.Add(Documents.Run(alteredStr))
+            // The right part part.
+            txtImageInformation2.Inlines.Add(Documents.Run("Added infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg true) + " " + (state.ImageManuallyChangedRBCStr srcImg true)))
             txtImageInformation2.Inlines.Add(Documents.LineBreak())
+            txtImageInformation2.Inlines.Add(Documents.Run("Removed infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg false) + " " + (state.ImageManuallyChangedRBCStr srcImg false)))
 
-            txtImageInformation2.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
-            txtImageInformation2.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString()))
-        | _ -> ()
+        | _ ->
+            gridImageInformation.Visibility <- Visibility.Hidden
 
     let updateGlobalParasitemia () =
-        txtGlobalParasitemia.Text <- percentText state.GlobalParasitemia
+        txtGlobalParasitemia.Inlines.Clear()
+        let total, infected = state.GlobalParasitemia
+        txtGlobalParasitemia.Inlines.Add(Documents.Run(Utils.percentText (total, infected), FontWeight = FontWeights.Bold))
+        if total > 0 && total < warningBelowNumberOfRBC
+        then
+            txtGlobalParasitemia.Inlines.Add(
+                Documents.Run(
+                    sprintf " Warning: the number of erytrocytes should be above %d" warningBelowNumberOfRBC,
+                    FontWeight = FontWeights.Bold,
+                    Foreground = Brushes.Red))
 
     let updateViewportPreview () =
         for preview in stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> do
@@ -425,6 +443,26 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         state.Reset()
         updateGUI()
 
+    let exportResults () =
+        let extension = ".txt"
+        let dialog = SaveFileDialog(AddExtension = true, DefaultExt = extension)
+
+        if state.FilePath <> ""
+        then
+            dialog.FileName <- Path.GetFileNameWithoutExtension(state.FilePath) + extension
+        elif state.PatientID <> ""
+        then
+            dialog.FileName <- state.PatientID + extension
+
+        let res = dialog.ShowDialog()
+        if res.HasValue && res.Value then
+            try
+                Export.exportResults state dialog.FileName
+            with
+            | :? IOException as ex ->
+                Log.Error(ex.ToString())
+                MessageBox.Show(sprintf "The results cannot be exported in '%s'" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
+
     txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
 
     menuExit.Click.AddHandler(fun obj args -> mainWindow.Root.Close())
@@ -432,6 +470,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     menuSaveAsFile.Click.AddHandler(fun obj args -> saveCurrentDocumentAsNewFile ())
     menuLoadFile.Click.AddHandler(fun obj args -> askLoadFile ())
     menuNewFile.Click.AddHandler(fun obj args -> newFile ())
+    menuExportResults.Click.AddHandler(fun obj args -> exportResults ())
 
     menuAddSourceImage.Click.AddHandler(fun obj args ->
         let dialog = OpenFileDialog(Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true)
@@ -579,10 +618,17 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             FSharp.ViewModule.FunCommand((fun obj -> newFile ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.N, Input.ModifierKeys.Control))) |> ignore
 
+    // Export results.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> exportResults ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.E, Input.ModifierKeys.Control))) |> ignore
+
     // Viewport preview.
     scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
 
     updateDocumentStatus ()
+    gridImageInformation.Visibility <- Visibility.Hidden
 
     mainWindow.Root.Show()
 
index 483888f..178fe7c 100644 (file)
@@ -68,8 +68,6 @@
     <Resource Include="Resources\chuv_logo.png" />
     <Resource Include="Resources\hes-so_logo.png" />
     <Resource Include="Resources\icon.ico" />
-    <Resource Include="XAML\NumericUpDown.xaml" />
-    <Compile Include="XAML\NumericUpDown.xaml.fs" />
     <Resource Include="XAML\ImageSourcePreview.xaml" />
     <Compile Include="XAML\ImageSourcePreview.xaml.fs" />
     <Resource Include="XAML\ImageSourceSelection.xaml" />
     <Resource Include="XAML\MainWindow.xaml" />
     <Compile Include="XAML\MainWindow.xaml.fs" />
     <Compile Include="Types.fs" />
+    <Compile Include="Utils.fs" />
     <Compile Include="PiaZ.fs" />
     <Compile Include="State.fs" />
+    <Compile Include="Export.fs" />
     <Compile Include="About.fs" />
     <Compile Include="Analysis.fs" />
     <Compile Include="GUI.fs" />
index c9dfd5b..92397be 100644 (file)
@@ -26,6 +26,8 @@ type JSONInformation = {
 // Information associated to each images.
 type JSONSourceImage = {
     num: int
+    name: string
+
     RBCRadius: float32 // The RBC Radius found by granulometry.
     parameters: ParasitemiaCore.Config.Parameters
     dateLastAnalysis: DateTime
@@ -42,7 +44,7 @@ type DocumentData = {
 
 let mainEntryName = "info.json"
 let imageExtension = ".tiff"
-let currentFileVersion = 1
+let currentFileVersion = 2
 
 /// <summary>
 /// Save a document in a give file path. The file may already exist.
@@ -72,6 +74,7 @@ let save (filePath: string) (data: DocumentData) =
         imgJSONFileWriter.Write(
             JsonConvert.SerializeObject(
                 { num = srcImg.num
+                  name = srcImg.name
                   RBCRadius = srcImg.config.RBCRadius.Pixel
                   parameters = srcImg.config.Parameters
                   dateLastAnalysis = srcImg.dateLastAnalysis
@@ -108,12 +111,13 @@ let load (filePath: string) : DocumentData =
                             use bitmap = new System.Drawing.Bitmap(imgEntry.Open(), false)
                             let img = new Image<Bgr, byte>(bitmap)
                             imgNum <- imgNum + 1
-                            let imgEntry = file.GetEntry(imgEntry.Name + ".json")
-                            use imgEntryFileReader = new StreamReader(imgEntry.Open())
-                            let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage>(imgEntryFileReader.ReadToEnd())
+                            let imgJSONEntry = file.GetEntry(imgEntry.Name + ".json")
+                            use imgJSONFileReader = new StreamReader(imgJSONEntry.Open())
+                            let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage>(imgJSONFileReader.ReadToEnd())
                             let config = ParasitemiaCore.Config.Config(imgInfo.parameters)
                             config.SetRBCRadius imgInfo.RBCRadius
                             yield { num = imgNum
+                                    name = imgInfo.name
                                     config = config
                                     dateLastAnalysis = imgInfo.dateLastAnalysis
                                     img = img
index 24e33cd..feab9ec 100644 (file)
@@ -77,7 +77,8 @@ let main args =
                         match ParasitemiaCore.Analysis.doMultipleAnalysis images None with
                         | Some results ->
                             for id, cells in results do
-                                let config = images |> List.pick (fun (id', config', _) -> if id' = id then Some config' else None)
+                                let config, img = images |> List.pick (fun (id', config', img') -> if id' = id then Some (config', img') else None)
+                                img.Dispose()
                                 let total, infected = countCells cells
                                 fprintf resultFile "File: %s %d %d %.2f (diameter: %A)\n" id total infected (100. * (float infected) / (float total)) config.RBCRadius
                         | None ->
index 63b9bbf..96dfc24 100644 (file)
@@ -30,9 +30,24 @@ type State () =
         List.length srcImg.rbcs,
         srcImg.rbcs |> List.fold (fun nbInfected rbc -> if rbc.infected then nbInfected + 1 else nbInfected) 0
 
-    member this.ImageNbAltered (srcImg: SourceImage) : int * int =
+    member this.ImageNbManuallyChangedRBC (srcImg: SourceImage) (setAsInfected: bool) : int * int =
         List.length srcImg.rbcs,
-        srcImg.rbcs |> List.fold (fun nbAltered rbc -> if rbc.setManually then nbAltered + 1 else nbAltered) 0
+        srcImg.rbcs |> List.fold (fun nb rbc -> if rbc.setManually && rbc.infected = setAsInfected then nb + 1 else nb) 0
+
+    member this.ImageNbManuallyChangedRBCStr (srcImg: SourceImage) (setAsInfected: bool) : string =
+        Utils.percentText (this.ImageNbManuallyChangedRBC srcImg setAsInfected)
+
+    member this.ImageManuallyChangedRBC (srcImg: SourceImage) (setAsInfected: bool) : int seq =
+        query {
+            for rbc in srcImg.rbcs do
+            where (rbc.setManually && rbc.infected = setAsInfected)
+            select rbc.num }
+
+    member this.ImageManuallyChangedRBCStr (srcImg: SourceImage) (setAsInfected: bool) : string =
+        let listStr = Utils.listAsStr <| this.ImageManuallyChangedRBC srcImg setAsInfected
+        if listStr = ""
+        then ""
+        else "[" + listStr + "]"
 
     member this.GlobalParasitemia : int * int =
         sourceImages
@@ -75,6 +90,7 @@ type State () =
     member this.AddSourceImage (filePath: string) (defaultConfig: ParasitemiaCore.Config.Config) : SourceImage =
         let srcImg =
             { num = sourceImages.Count + 1
+              name = System.IO.FileInfo(filePath).Name
               config = defaultConfig.Copy()
               dateLastAnalysis = DateTime(0L)
               rbcs = []
@@ -103,6 +119,12 @@ type State () =
         // Re-numbered the images.
         sourceImages |> Seq.iteri (fun i srcImg -> srcImg.num <- i + 1)
 
+    member this.SetName (srcImg: SourceImage) (name: string) =
+        if name <> srcImg.name
+        then
+            srcImg.name <- name
+            alteredSinceLastSave <- true
+
     member this.SetResult (imgNum: int) (cells: ParasitemiaCore.Types.Cell list) =
         let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = imgNum)
 
index 5de71e6..92d39b8 100644 (file)
@@ -22,6 +22,8 @@ type RBC = {
 
 type SourceImage = {
     mutable num: int
+    mutable name: string
+
     mutable config: ParasitemiaCore.Config.Config
     mutable dateLastAnalysis: DateTime // UTC.
     img: Image<Bgr, byte>
diff --git a/Parasitemia/ParasitemiaUI/Utils.fs b/Parasitemia/ParasitemiaUI/Utils.fs
new file mode 100644 (file)
index 0000000..324e5f0
--- /dev/null
@@ -0,0 +1,12 @@
+module ParasitemiaUI.Utils
+
+let listAsStr (s: 'a seq) =
+    s |> Seq.fold (fun acc obj -> acc + (if acc = "" then "" else ", ") + obj.ToString()) ""
+
+let percentText (nbTotal: int, nb: int) : string =
+    if nbTotal = 0
+    then
+        ""
+    else
+        let percent = 100. * (float nb) / (float nbTotal)
+        sprintf "%.1f %% (%d / %d)" percent nb nbTotal
\ No newline at end of file
index e763bea..000da4e 100644 (file)
             <MenuItem x:Name="menuSave" Header="_Save" InputGestureText="Ctrl+S" />
             <MenuItem x:Name="menuSaveAs" Header="Save _As..." InputGestureText="Ctrl+Shift+S" />
             <Separator />
+            <MenuItem x:Name="menuExportResults" Header="E_xport Results As Text..." InputGestureText="Ctrl+E" />
+            <Separator />
             <MenuItem x:Name="menuExit" Header="_Exit" />
          </MenuItem>
          <MenuItem Header="_Images">
-            <MenuItem x:Name="menuAddSourceImage" Header="_Add a source image" />
+            <MenuItem x:Name="menuAddSourceImage" Header="_Add a Source Image" />
          </MenuItem>
          <MenuItem x:Name="menuAnalysis" Header="_Analysis">
-            <MenuItem x:Name="menuStartAnalysis" Header="_Show analysis window" />
+            <MenuItem x:Name="menuStartAnalysis" Header="_Show Analysis Window" />
          </MenuItem>
          <MenuItem x:Name="menuView" Header="_View">
-            <MenuItem x:Name="menuHightlightRBC" Header="_Highlight all erytrocytes" IsCheckable="True" />
+            <MenuItem x:Name="menuHightlightRBC" Header="_Highlight All Erytrocytes" IsCheckable="True" />
          </MenuItem>
          <MenuItem x:Name="menuHelp" Header="_Help">
             <MenuItem x:Name="menuAbout" Header="_About" />
@@ -57,7 +59,9 @@
             <Label x:Name="lblPatient" Margin="10,0,3,0 " Content="Patient ID" Grid.ColumnSpan="2"/>
             <Label x:Name="lblGlobalParasitemia" Margin="10,0,3,0" Content="Global parasitemia" Grid.Row="1" Grid.ColumnSpan="2" />
             <TextBox x:Name="txtPatient" Grid.Column="2" Margin="3,4,10,4" TextWrapping="Wrap" VerticalAlignment="Center" />
-            <TextBox x:Name="txtGlobalParasitemia" Grid.Column="2" Grid.Row="1" Margin="3,4,10,4" TextWrapping="Wrap" VerticalAlignment="Center" IsReadOnly="True" />
+            <Border BorderThickness="1" VerticalAlignment="Center" Margin="3,4,10,4" BorderBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" Grid.Column="2" Grid.Row="1" >
+               <TextBlock x:Name="txtGlobalParasitemia" Margin="1" TextWrapping="Wrap"  />
+            </Border>
          </Grid>
          <Border BorderBrush="Black" BorderThickness="0"  Margin="3" Grid.Row="1" >
             <ScrollViewer x:Name="scrollPreviews" VerticalScrollBarVisibility="Auto" >
             <ScrollViewer x:Name="scrollRBC" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible" Grid.RowSpan="1"  Margin="3">
                <StackPanel x:Name="stackRBC" Orientation="Horizontal" />
             </ScrollViewer>
-            <Grid Grid.Row="2">
+            <Grid x:Name="gridImageInformation" Grid.Row="2">
                <Grid.ColumnDefinitions>
                   <ColumnDefinition Width="1*"/>
                   <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>
-               <TextBlock x:Name="txtImageInformation1" TextWrapping="Wrap" Margin="3" Grid.Column="0" />
-               <TextBlock x:Name="txtImageInformation2" TextWrapping="Wrap" Margin="3" Grid.Column="1" />
+               <Grid.RowDefinitions>
+                  <RowDefinition Height="Auto" />
+                  <RowDefinition Height="1*" />
+               </Grid.RowDefinitions>
+               <Grid Grid.ColumnSpan="2" Grid.Row="0" Margin="0,3,3,0">
+                  <Grid.ColumnDefinitions>
+                     <ColumnDefinition Width="Auto"/>
+                     <ColumnDefinition Width="1*"/>
+                  </Grid.ColumnDefinitions>
+                  <Label Content="Image name" Grid.Column="0"></Label>
+                  <TextBox x:Name="txtImageName" Grid.Column="1" VerticalAlignment="Center"> </TextBox>
+               </Grid>
+               <TextBlock x:Name="txtImageInformation1" TextWrapping="Wrap" Margin="3" Grid.Column="0" Grid.Row="1" />
+               <TextBlock x:Name="txtImageInformation2" TextWrapping="Wrap" Margin="3" Grid.Column="1" Grid.Row="1" />
             </Grid>
          </Grid>
       </Grid>