GUI (work in progress..)
authorGreg Burri <greg.burri@gmail.com>
Sun, 10 Jan 2016 16:29:19 +0000 (17:29 +0100)
committerGreg Burri <greg.burri@gmail.com>
Sun, 10 Jan 2016 16:29:19 +0000 (17:29 +0100)
Parasitemia/Parasitemia/Config.fs
Parasitemia/Parasitemia/GUI/GUI.fs
Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml
Parasitemia/Parasitemia/GUI/Pia.fs [deleted file]
Parasitemia/Parasitemia/GUI/PiaZ.fs [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/State.fs
Parasitemia/Parasitemia/GUI/Types.fs
Parasitemia/Parasitemia/MainAnalysis.fs
Parasitemia/Parasitemia/Parasitemia.fsproj
Parasitemia/Parasitemia/Program.fs

index 701a3fd..5bb9936 100644 (file)
@@ -20,7 +20,7 @@ type Parameters = {
     factorNbPick: float
 
     // Parasites detection.
-    darkStainLevel: float
+    darkStainLevel: float // Lower -> more sensitive. Careful about illumination on the borders.
     maxDarkStainRatio: float
 
     stainArea: float32 // Factor of a RBC area. 0.5 means the half of RBC area.
index c6e6390..2aadefe 100644 (file)
@@ -10,8 +10,6 @@ open System.Windows.Controls
 open System.Diagnostics
 open Microsoft.Win32 // For the common dialogs.
 
-//open Emgu.CV
-//open Emgu.CV.Structure
 open Emgu.CV.WPF
 
 open Config
@@ -25,8 +23,8 @@ let run (defaultConfig: Config) =
 
     // Utils.log <- (fun m -> log mainWindow m)
 
-    let colorRBCHealthy = Brushes.Green
-    let colorRBCInfected = Brushes.Yellow
+    let colorRBCHealthy = Brushes.YellowGreen
+    let colorRBCInfected = Brushes.Red
 
     let state = State.State()
 
@@ -51,22 +49,44 @@ let run (defaultConfig: Config) =
         state.PatientID <- txtPatient.Text
 
     let setCurrentImage (srcImg: SourceImage) =
+        state.CurrentImage <- Some srcImg
+
+        // Highlight the preview.
+        stackPreviews.Children
+        |> Seq.cast<Views.ImageSourcePreview>
+        |> Seq.iter (fun preview -> preview.border.BorderThickness <- Thickness(if preview.lblImageNumber.Content = box srcImg.num then 3. else 0.))
+
         canvasCurrentImage.Height <- float srcImg.img.Height
         canvasCurrentImage.Width <- float srcImg.img.Width
         canvasCurrentImage.Background <- ImageBrush(BitmapSourceConvert.ToBitmapSource(srcImg.img))
 
         // Remove all image canvas children and add the RBC.
         canvasCurrentImage.Children.Clear()
-        for rbc in (*srcImg.rbcs*) [{ num = 1; infected = true; addedManually = false; removed = false; center = Point(100., 100.); size = Size(40., 40.); stainArea = 10 }] do
+        for rbc in srcImg.rbcs do
             let rectangle =
                 Rectangle(
                     Height = rbc.size.Height,
                     Width = rbc.size.Width,
                     Stroke = (if rbc.infected then colorRBCInfected else colorRBCHealthy),
-                    StrokeThickness = 1.)
-            Canvas.SetLeft(rectangle, rbc.center.X + rbc.size.Width / 2.)
-            Canvas.SetTop(rectangle, rbc.center.Y + rbc.size.Height / 2.)
+                    StrokeThickness = 1.,
+                    Fill = SolidColorBrush(Color.FromArgb(0uy, 0uy, 0uy, 0uy)),
+                    Tag = rbc,
+                    Opacity = if rbc.infected then 1. else 0.)
+            Canvas.SetLeft(rectangle, rbc.center.X - rbc.size.Width / 2.)
+            Canvas.SetTop(rectangle, rbc.center.Y - rbc.size.Height / 2.)
             canvasCurrentImage.Children.Add(rectangle) |> ignore
+            rectangle.MouseEnter.AddHandler(
+                fun obj args -> match obj with
+                                | :? Rectangle as r ->
+                                    r.StrokeThickness <- 3.
+                                    if not (r.Tag :?> RBC).infected then r.Opacity <- 1.
+                                | _ -> ())
+            rectangle.MouseLeave.AddHandler(
+                fun obj args -> match obj with
+                                | :? Rectangle as r ->
+                                    r.StrokeThickness <- 1.
+                                    if not (r.Tag :?> RBC).infected then r.Opacity <- 0.
+                                | _ -> ())
 
     let addPreview (srcImg: SourceImage) =
         let imgCtrl = Views.ImageSourcePreview(Margin = Thickness(3.))
@@ -75,6 +95,7 @@ let run (defaultConfig: Config) =
         let height = srcImg.img.Height * width / srcImg.img.Width
         imgCtrl.imagePreview.Source <- BitmapSourceConvert.ToBitmapSource(srcImg.img.Resize(width, height, Emgu.CV.CvEnum.Inter.Cubic))
         stackPreviews.Children.Add(imgCtrl) |> ignore
+        imgCtrl.MouseLeftButtonUp.AddHandler(fun obj args -> setCurrentImage (state.SourceImages |> Seq.find (fun i -> box i.num = (obj :?> Views.ImageSourcePreview).lblImageNumber.Content)))
 
     let updatePreviews () =
         stackPreviews.Children.Clear ()
@@ -93,7 +114,7 @@ let run (defaultConfig: Config) =
         synchronizeState ()
         if state.FilePath = ""
         then
-            let dialog = SaveFileDialog(AddExtension = true, DefaultExt = Pia.extension, Filter = Pia.filter);
+            let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter);
             let res = dialog.ShowDialog()
             if res.HasValue && res.Value
             then
@@ -104,7 +125,7 @@ let run (defaultConfig: Config) =
 
     loadFile.Click.AddHandler(fun obj args ->
         // TODO: if current state not saved and not empty, ask to save it.
-        let dialog = OpenFileDialog(Filter = Pia.filter)
+        let dialog = OpenFileDialog(Filter = PiaZ.filter)
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
         then
@@ -128,7 +149,11 @@ let run (defaultConfig: Config) =
             then
                 setCurrentImage srcImg)
 
-    butStartAnalysis.Click.AddHandler(fun obj args -> ())
+    butStartAnalysis.Click.AddHandler(fun obj args ->
+        let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig
+        for id, cells in results do
+            state.SetResult (int id) cells
+        )
 
     // Zoom on the current image.
     let adjustCurrentImageBorders (deltaX: float) (deltaY: float) =
@@ -158,8 +183,8 @@ let run (defaultConfig: Config) =
         scrollViewCurrentImage.ScrollToVerticalOffset(scrollViewCurrentImage.VerticalOffset + deltaY / 8.))
 
     let mutable currentScale = 1.
-    let mutable maxScale = 5.
-    let mutable minScale = 0.1
+    let mutable maxScale = 4.
+    let mutable minScale = 0.25
     let currentImageScaleTransform = ScaleTransform()
     canvasCurrentImage.LayoutTransform <- currentImageScaleTransform
     borderCurrentImage.PreviewMouseWheel.AddHandler(fun obj args ->
@@ -212,14 +237,6 @@ let run (defaultConfig: Config) =
             borderCurrentImage.ReleaseMouseCapture()
             args.Handled <- true)
 
-    (* let txtPatient: Controls.TextBox = ctrl "txtPatient"
-    txtPatient.TextChanged.AddHandler(fun obj args ->
-        state.PatientID <- txtPatient.Text) *)
-
-     (* saveFileDialog1.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"  ;
-     saveFileDialog1.FilterIndex = 2 ;
-     saveFileDialog1.RestoreDirectory = true ; *)
-
     // display mainWindow img
     mainWindow.Root.Show()
     app.Run()
\ No newline at end of file
index 3593e0e..6fd7ba2 100644 (file)
@@ -6,8 +6,10 @@
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       mc:Ignorable="d" d:DesignWidth="119.223" d:DesignHeight="84.911"
                >
-   <Grid>
-      <Image x:Name="imagePreview" />
-      <Label x:Name="lblImageNumber" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White"/>
-   </Grid>
+   <Border x:Name="border" BorderBrush="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}">
+      <Grid>
+         <Image x:Name="imagePreview" />
+         <Label x:Name="lblImageNumber" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White"/>
+      </Grid>
+   </Border>
 </UserControl>
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/Pia.fs b/Parasitemia/Parasitemia/GUI/Pia.fs
deleted file mode 100644 (file)
index 1821f26..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-// ParasitemIA file format.
-module Parasitemia.GUI.Pia
-
-open System.Windows
-open System.IO
-open System.IO.Compression
-
-open FSharp.Data
-
-open Emgu.CV
-open Emgu.CV.Structure
-
-open Types
-
-let extension = ".pia"
-let filter = "PIA|*.pia"
-
-type FileData = {
-    sources: SourceImage list
-    patientID: string }
-
-// The json type associated to a source image.
-type JSONSourceImage = JsonProvider<"""
-    {
-        "rbcs": [
-            {
-                "num": 1,
-                "infected": true,
-                "addedManually": false,
-                "removed": false,
-                "posX" : 42.5,
-                "posY" : 42.5,
-                "width" : 10.5,
-                "height" : 10.5,
-                "stainArea" : 10
-            }
-        ]
-    }
-""">
-
-// The json type associated to a file.
-type JSONMainInformation = JsonProvider<"""
-    {
-        "patientID": "1234abcd"
-    }
-""">
-
-let mainFilename = "info.json"
-let imageExtension = ".tiff"
-
-let save (filePath: string) (data: FileData) =
-    use file = ZipFile.Open(filePath, ZipArchiveMode.Update)
-
-    for e in List.ofSeq file.Entries do // 'ofSeq' to not iterate a collection currently modified.
-        e.Delete()
-
-    // Main JSON file.
-    let mainJSON = JSONMainInformation.Root(data.patientID)
-    let mainFile = file.CreateEntry(mainFilename, CompressionLevel.Fastest)
-    use mainFileWriter = new StreamWriter(mainFile.Open())
-    mainJSON.JsonValue.WriteTo(mainFileWriter, JsonSaveOptions.None)
-
-    // Write each images and the associated information.
-    for imgSrc in data.sources do
-        let imgFilename = (string imgSrc.num) + imageExtension
-        let imgEntry = file.CreateEntry(imgFilename, CompressionLevel.NoCompression) // FIXME: It seems a compression is applied to this file despite of the 'NoCompression' flag.
-        imgSrc.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
-
-        let imgJSON =
-            JSONSourceImage.Root([| for rbc in imgSrc.rbcs ->
-                                        JSONSourceImage.Rbc(
-                                            rbc.num,
-                                            rbc.infected, rbc.addedManually, rbc.removed,
-                                            decimal rbc.center.X, decimal rbc.center.Y, decimal rbc.size.Width, decimal rbc.size.Height,
-                                            rbc.stainArea) |])
-
-        let imgJSONEntry = file.CreateEntry(imgFilename + ".json", CompressionLevel.Fastest)
-        use imgJSONFileWriter = new StreamWriter(imgJSONEntry.Open())
-        imgJSON.JsonValue.WriteTo(imgJSONFileWriter, JsonSaveOptions.None)
-
-
-let load (filePath: string) : FileData =
-    use file = ZipFile.Open(filePath, ZipArchiveMode.Read)
-
-    let mainFile = file.GetEntry(mainFilename)
-    let mainJSON = JSONMainInformation.Load(mainFile.Open())
-
-    let sources = [
-        let mutable imgNum = 0
-        for imgEntry in file.Entries do
-            let filename = imgEntry.Name
-            if filename.EndsWith(imageExtension)
-            then
-                let img = new Image<Bgr, byte>(new System.Drawing.Bitmap(imgEntry.Open(), false)) // FIXME: Should we dispose the bitmap?
-                imgNum <- imgNum + 1
-                let imgJSONEntry = file.GetEntry(filename + ".json")
-                let imgJSON = JSONSourceImage.Load(imgJSONEntry.Open())
-                yield { num = imgNum; img = img; rbcs = [ for rbc in imgJSON.Rbcs ->
-                                                            { num = rbc.Num;
-                                                              infected = rbc.Infected; addedManually = rbc.AddedManually; removed = rbc.Removed;
-                                                              center = Point(float rbc.PosX, float rbc.PosY); size = Size(float rbc.Width, float rbc.Height);
-                                                              stainArea = rbc.StainArea } ] } ]
-    { sources = sources; patientID = mainJSON.PatientId }
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/PiaZ.fs b/Parasitemia/Parasitemia/GUI/PiaZ.fs
new file mode 100644 (file)
index 0000000..900ec65
--- /dev/null
@@ -0,0 +1,102 @@
+// ParasitemIA Zipped file format.
+module Parasitemia.GUI.PiaZ
+
+open System.Windows
+open System.IO
+open System.IO.Compression
+
+open FSharp.Data
+
+open Emgu.CV
+open Emgu.CV.Structure
+
+open Types
+
+let extension = ".piaz"
+let filter = "PIA|*.piaz"
+
+type FileData = {
+    sources: SourceImage list
+    patientID: string }
+
+// The json type associated to a source image.
+type JSONSourceImage = JsonProvider<"""
+    {
+        "rbcs": [
+            {
+                "num": 1,
+                "infected": true,
+                "setManually": false,
+                "posX" : 42.5,
+                "posY" : 42.5,
+                "width" : 10.5,
+                "height" : 10.5,
+                "stainArea" : 10
+            }
+        ]
+    }
+""">
+
+// The json type associated to a file.
+type JSONMainInformation = JsonProvider<"""
+    {
+        "patientID": "1234abcd"
+    }
+""">
+
+let mainFilename = "info.json"
+let imageExtension = ".tiff"
+
+let save (filePath: string) (data: FileData) =
+    use file = ZipFile.Open(filePath, ZipArchiveMode.Update)
+
+    for e in List.ofSeq file.Entries do // 'ofSeq' to not iterate a collection currently modified.
+        e.Delete()
+
+    // Main JSON file.
+    let mainJSON = JSONMainInformation.Root(data.patientID)
+    let mainFile = file.CreateEntry(mainFilename, CompressionLevel.Fastest)
+    use mainFileWriter = new StreamWriter(mainFile.Open())
+    mainJSON.JsonValue.WriteTo(mainFileWriter, JsonSaveOptions.None)
+
+    // Write each images and the associated information.
+    for imgSrc in data.sources do
+        let imgFilename = (string imgSrc.num) + imageExtension
+        let imgEntry = file.CreateEntry(imgFilename, CompressionLevel.NoCompression) // FIXME: It seems a compression is applied to this file despite of the 'NoCompression' flag.
+        imgSrc.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
+
+        let imgJSON =
+            JSONSourceImage.Root([| for rbc in imgSrc.rbcs ->
+                                        JSONSourceImage.Rbc(
+                                            rbc.num,
+                                            rbc.infected, rbc.setManually,
+                                            decimal rbc.center.X, decimal rbc.center.Y, decimal rbc.size.Width, decimal rbc.size.Height,
+                                            rbc.stainArea) |])
+
+        let imgJSONEntry = file.CreateEntry(imgFilename + ".json", CompressionLevel.Fastest)
+        use imgJSONFileWriter = new StreamWriter(imgJSONEntry.Open())
+        imgJSON.JsonValue.WriteTo(imgJSONFileWriter, JsonSaveOptions.None)
+
+
+let load (filePath: string) : FileData =
+    use file = ZipFile.Open(filePath, ZipArchiveMode.Read)
+
+    let mainFile = file.GetEntry(mainFilename)
+    let mainJSON = JSONMainInformation.Load(mainFile.Open())
+
+    let sources = [
+        let mutable imgNum = 0
+        for imgEntry in file.Entries do
+            let filename = imgEntry.Name
+            if filename.EndsWith(imageExtension)
+            then
+                let img = new Image<Bgr, byte>(new System.Drawing.Bitmap(imgEntry.Open(), false)) // FIXME: Should we dispose the bitmap?
+                imgNum <- imgNum + 1
+                let imgJSONEntry = file.GetEntry(filename + ".json")
+                let imgJSON = JSONSourceImage.Load(imgJSONEntry.Open())
+                yield { num = imgNum; img = img; rbcs = [ for rbc in imgJSON.Rbcs ->
+                                                            { num = rbc.Num;
+                                                              infected = rbc.Infected; setManually = rbc.SetManually;
+                                                              center = Point(float rbc.PosX, float rbc.PosY); size = Size(float rbc.Width, float rbc.Height);
+                                                              stainArea = rbc.StainArea } ] } ]
+    { sources = sources; patientID = mainJSON.PatientId }
\ No newline at end of file
index 4502536..a1ae052 100644 (file)
@@ -1,6 +1,7 @@
 module Parasitemia.GUI.State
 
 open System.Collections.Generic
+open System.Windows
 
 open Emgu.CV
 open Emgu.CV.Structure
@@ -9,17 +10,17 @@ open Types
 
 type State () =
     let sourceImages = List<SourceImage>()
-    let mutable currentImage = -1
 
+    member val CurrentImage: SourceImage option = None with get, set
     member val FilePath: string = "" with get, set
     member val PatientID: string = "" with get, set
 
     member this.Save () =
-        let data = { Pia.sources = List.ofSeq sourceImages; Pia.patientID = this.PatientID }
-        Pia.save this.FilePath data
+        let data = { PiaZ.sources = List.ofSeq sourceImages; PiaZ.patientID = this.PatientID }
+        PiaZ.save this.FilePath data
 
     member this.Load () =
-        let data = Pia.load this.FilePath
+        let data = PiaZ.load this.FilePath
         this.PatientID <- data.patientID
         sourceImages.Clear()
         sourceImages.InsertRange(0, data.sources)
@@ -27,12 +28,21 @@ type State () =
     member this.AddSourceImage (filePath: string) : SourceImage =
         let srcImg = { num = sourceImages.Count + 1; rbcs = []; img = new Image<Bgr, byte>(filePath) }
         sourceImages.Add(srcImg)
-        if sourceImages.Count = 1
-        then
-            currentImage <- 1
         srcImg
 
-    member x.SourceImages : SourceImage seq =
+    member this.SetResult (num: int) (cells: Cell list) =
+        let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = num)
+        sourceImage.rbcs <- cells
+            |> List.filter (fun cell -> match cell.cellClass with HealthyRBC | InfectedRBC -> true | _ -> false )
+            |> List.mapi (fun i cell ->
+                { num = i
+                  infected = cell.cellClass = InfectedRBC
+                  setManually = false
+                  center = Point(float cell.center.X, float cell.center.Y)
+                  size = Size(float cell.elements.Width, float cell.elements.Height)
+                  stainArea = cell.stainArea })
+
+    member this.SourceImages : SourceImage seq =
         sourceImages :> SourceImage seq
 
     member this.Reset () =
index d6e9cf9..e912ad3 100644 (file)
@@ -9,8 +9,7 @@ type RBC = {
     num: int
 
     infected: bool
-    addedManually: bool
-    removed: bool
+    setManually: bool
 
     center: Point
     size: Size
@@ -19,4 +18,4 @@ type RBC = {
 type SourceImage = {
     num: int
     img: Image<Bgr, byte>
-    rbcs: RBC list }
\ No newline at end of file
+    mutable rbcs: RBC list }
\ No newline at end of file
index 043eb73..5ee6886 100644 (file)
@@ -3,6 +3,8 @@
 open System
 open System.Drawing
 
+open FSharp.Collections.ParallelSeq
+
 open Emgu.CV
 open Emgu.CV.Structure
 
@@ -91,3 +93,13 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) : Cell li
     | _ -> ()
 
     cells
+
+
+let doMultipleAnalysis (imgs: (string * Image<Bgr, byte>) list) (config : Config) : (string * Cell list) list =
+    let nbConcurrentTaskLimit = 4
+    let n = Environment.ProcessorCount
+
+    imgs
+    |> PSeq.map (fun (id, img) -> id, doAnalysis img id (config.Copy()))
+    |> PSeq.withDegreeOfParallelism (if n > nbConcurrentTaskLimit then nbConcurrentTaskLimit else n)
+    |> PSeq.toList
\ No newline at end of file
index 223be2c..d3289c6 100644 (file)
@@ -97,7 +97,7 @@
     <Resource Include="GUI\MainWindow.xaml" />
     <Compile Include="GUI\MainWindow.xaml.fs" />
     <Compile Include="GUI\Types.fs" />
-    <Compile Include="GUI\Pia.fs" />
+    <Compile Include="GUI\PiaZ.fs" />
     <Compile Include="GUI\State.fs" />
     <Compile Include="GUI\GUI.fs" />
     <Compile Include="Program.fs" />
index bb61f4f..711ec6f 100644 (file)
@@ -4,8 +4,6 @@ open System
 open System.IO
 open System.Threading
 
-open FSharp.Collections.ParallelSeq
-
 open Emgu.CV
 open Emgu.CV.Structure
 
@@ -56,7 +54,7 @@ let main args =
 
                 factorNbPick = 1.0
 
-                darkStainLevel = 0.25 // Lower -> more sensitive. 0.3. Careful about illumination on the borders.
+                darkStainLevel = 0.25 // 0.3
                 maxDarkStainRatio = 0.1 // 10 %
 
                 infectionArea = 0.012f // 1.2 %
@@ -88,16 +86,11 @@ let main args =
             use resultFile = new StreamWriter(new FileStream(Path.Combine(output, "results.txt"), FileMode.Append, FileAccess.Write))
 
             //try
-            let images = seq { for file in files -> Path.GetFileNameWithoutExtension(FileInfo(file).Name), new Image<Bgr, byte>(file) }
+            let images = [ for file in files -> Path.GetFileNameWithoutExtension(FileInfo(file).Name), new Image<Bgr, byte>(file) ]
 
-            let nbConcurrentTaskLimit = 4
-            let n = Environment.ProcessorCount
 
             Utils.logTime "Whole analyze" (fun () ->
-                let results =
-                    images
-                    |> PSeq.map (fun (id, img) -> id, ImageAnalysis.doAnalysis img id (config.Copy()))
-                    |> PSeq.withDegreeOfParallelism (if n > nbConcurrentTaskLimit then nbConcurrentTaskLimit else n)
+                let results = ImageAnalysis.doMultipleAnalysis images config
 
                 for id, cells in results do
                     let total, infected = Utils.countCells cells