* Add the analysis window.
authorGreg Burri <greg.burri@gmail.com>
Wed, 13 Jan 2016 10:45:14 +0000 (11:45 +0100)
committerGreg Burri <greg.burri@gmail.com>
Wed, 13 Jan 2016 10:45:14 +0000 (11:45 +0100)
* Other improvements.

18 files changed:
Parasitemia/Parasitemia/Config.fs
Parasitemia/Parasitemia/GUI/Analysis.fs [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml.fs [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/GUI.fs
Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml
Parasitemia/Parasitemia/GUI/MainWindow.xaml
Parasitemia/Parasitemia/GUI/PiaZ.fs
Parasitemia/Parasitemia/GUI/RBCFrame.xaml
Parasitemia/Parasitemia/GUI/State.fs
Parasitemia/Parasitemia/GUI/Types.fs
Parasitemia/Parasitemia/MainAnalysis.fs
Parasitemia/Parasitemia/Parasitemia.fsproj
Parasitemia/Parasitemia/ParasitesMarker.fs
Parasitemia/Parasitemia/Program.fs
Parasitemia/Parasitemia/Utils.fs
Parasitemia/Parasitemia/packages.config
Parasitemia/WPF/WPF.csproj

index 5bb9936..e583160 100644 (file)
@@ -9,41 +9,44 @@ type Debug =
     | DebugOn of string // Output directory.
 
 type Parameters = {
-    initialAreaOpen: int
+    initialAreaOpen: int // Area of the first initial opening to remove the central illumination of RBC.
+    ratioSecondAreaOpen: float32 // The area of the second opening is 'ratioSecondAreaOpen' * mean RBC area. It's applied only if greater than 'initialAreaOpen'.
 
-    minRbcRadius: float32
-    maxRbcRadius: float32
+    minRbcRadius: float32 // Factor of the mean RBC radius.
+    maxRbcRadius: float32 // Factor of the mean RBC radius.
 
-    preFilterSigma: float
+    preFilterSigma: float // To remove the high frequency noise.
 
     // Ellipse.
-    factorNbPick: float
+    factorNbPick: float // The number of computed ellipse per edge pixel.
 
     // Parasites detection.
     darkStainLevel: float // Lower -> more sensitive. Careful about illumination on the borders.
-    maxDarkStainRatio: float
+    maxDarkStainRatio: float // When a cell must own less than this ratio to be a RBC.
 
     stainArea: float32 // Factor of a RBC area. 0.5 means the half of RBC area.
     stainLevel: float // > 1
-    maxStainRatio: float // [0, 1]
+    maxStainRatio: float // When a cell must own less than this ratio to be a RBC.
 
     infectionArea: float32 // Factor of a RBC area. 0.5 means the half of RBC area.
     infectionLevel: float // > 1
 
     standardDeviationMaxRatio: float // The standard deviation of the pixel values of a cell can't be greater than standardDeviationMaxRatio * global standard deviation
-    minimumCellArea: float32 // Factor of the nominal RBC area.
+    minimumCellAreaFactor: float32 // Factor of the mean RBC area.
 }
 
 type Config (param: Parameters) =
     member this.Parameters = param
     member val Debug = DebugOff with get, set
+
+    // Mean RBC radius.
     member val RBCRadius = 30.f with get, set
 
     member this.RBCMinRadius = this.RBCRadius + param.minRbcRadius * this.RBCRadius
     member this.RBCMaxRadius = this.RBCRadius + param.maxRbcRadius * this.RBCRadius
 
     member this.RBCArea = PI * this.RBCRadius ** 2.f
-    member this.RBCMinArea = param.minimumCellArea * this.RBCArea
+    member this.RBCMinArea = param.minimumCellAreaFactor * this.RBCArea
 
     member this.InfectionArea = param.infectionArea * this.RBCArea
     member this.StainArea = param.stainArea * this.RBCArea
diff --git a/Parasitemia/Parasitemia/GUI/Analysis.fs b/Parasitemia/Parasitemia/GUI/Analysis.fs
new file mode 100644 (file)
index 0000000..acb9ce2
--- /dev/null
@@ -0,0 +1,75 @@
+module Parasitemia.GUI.Analysis
+
+open System
+open System.Windows
+open System.Windows.Media
+open System.Windows.Markup
+open System.Windows.Shapes
+open System.Windows.Controls
+
+open Config
+
+let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : bool =
+    let window = Views.AnalysisWindow()
+    window.Root.Owner <- parent
+    window.Root.Left <- parent.Left + parent.ActualWidth / 2. - window.Root.Width / 2.
+    window.Root.Top <- parent.Top + parent.ActualHeight / 2. - window.Root.Height / 2.
+
+    let ctrl (name: string): 'a =
+        window.Root.FindName(name) :?> 'a
+
+    let butClose: Button = ctrl "butClose"
+    let butStart: Button = ctrl "butStart"
+
+    let progressBar: ProgressBar = ctrl "progress"
+    let textLog: TextBlock = ctrl "textLog"
+    let scrollLog: ScrollViewer = ctrl "scrollLog"
+
+    Utils.log <- (fun mess -> window.Root.Dispatcher.Invoke(fun () ->
+        textLog.Inlines.Add(Documents.Run(mess))
+        textLog.Inlines.Add(Documents.LineBreak())
+        scrollLog.ScrollToBottom()))
+
+    let monitor = Object()
+    let mutable analysisPerformed = false
+    let mutable analysisCancelled = false
+
+    butClose.Click.AddHandler(fun obj args -> window.Root.Close())
+
+    butStart.Click.AddHandler(fun obj args ->
+        butStart.IsEnabled <- false
+        butClose.Content <- "Abort"
+        async {
+            let results =
+                ImageAnalysis.doMultipleAnalysis
+                    (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList)
+                    defaultConfig
+                    (Some (fun progress -> window.Root.Dispatcher.Invoke(fun () -> progressBar.Value <- float progress)))
+
+            lock monitor (
+                fun() ->
+                    if not analysisCancelled
+                    then
+                        for id, rbcRadius, cells in results do
+                            state.SetResult (int id) rbcRadius cells
+
+                        window.Root.Dispatcher.Invoke(fun () ->
+                            butStart.IsEnabled <- false
+                            butClose.Content <- "Close")
+
+                        Utils.log "Analysis terminated successfully"
+                        analysisPerformed <- true)
+        } |> Async.Start)
+
+    window.Root.ShowDialog() |> ignore
+
+    lock monitor (fun () ->
+        if not analysisPerformed
+            then
+                analysisCancelled <- true
+        analysisPerformed)
+
+
+    (*let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig
+    for id, rbcRadius, cells in results do
+        state.SetResult (int id) rbcRadius cells*)
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml b/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml
new file mode 100644 (file)
index 0000000..6da0675
--- /dev/null
@@ -0,0 +1,30 @@
+<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        mc:Ignorable="d"
+        x:Name="AnalysisWindow" Height="350" Width="500" MinHeight="100" MinWidth="100" Title="Analysis">
+   <Grid>
+      <Grid.RowDefinitions>
+         <RowDefinition Height="100"/>
+         <RowDefinition Height="30"/>
+         <RowDefinition/>
+         <RowDefinition Height="Auto"/>
+      </Grid.RowDefinitions>
+      <ScrollViewer x:Name="scrollImagesSourceSelection" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible" Grid.Row="0" Margin="3" >
+         <StackPanel x:Name="stackImagesSourceSelection" Orientation="Horizontal" />
+      </ScrollViewer>
+      <ProgressBar x:Name="progress" Grid.Row="1" Margin="3" Minimum="0" Maximum="100" />
+      <ScrollViewer x:Name="scrollLog" Grid.Row="2" Margin="3" HorizontalScrollBarVisibility="Auto">
+         <TextBlock x:Name="textLog" />
+      </ScrollViewer>
+      <Grid Grid.Row="3">
+         <Grid.ColumnDefinitions>
+            <ColumnDefinition/>
+            <ColumnDefinition/>
+         </Grid.ColumnDefinitions>
+         <Button x:Name="butStart" Content="Start analysis"  Margin="3" Grid.Column="0"/>
+         <Button x:Name="butClose" Content="Close"  Margin="3" Grid.Column="1"/>
+      </Grid>
+   </Grid>
+</Window>
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml.fs b/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml.fs
new file mode 100644 (file)
index 0000000..78cc682
--- /dev/null
@@ -0,0 +1,6 @@
+namespace Parasitemia.GUI.Views
+
+open FsXaml
+
+type AnalysisWindow = XAML<"GUI/AnalysisWindow.xaml">
+
index 8aef251..f88bc3c 100644 (file)
@@ -1,5 +1,6 @@
 module Parasitemia.GUI.Main
 
+open System
 open System.IO
 open System.Linq
 open System.Windows
@@ -21,25 +22,24 @@ let run (defaultConfig: Config) =
     let ctrl (name: string): 'a =
         mainWindow.Root.FindName(name) :?> 'a
 
-    // Utils.log <- (fun m -> log mainWindow m)
-
     let colorRBCHealthy = Brushes.YellowGreen
     let colorRBCInfected = Brushes.Red
 
     let state = State.State()
     let mutable currentScale = 1.
+    let mutable displayHealthy = false
 
-    let exit: MenuItem = ctrl "menuExit"
-    let saveFile: MenuItem = ctrl "menuSave"
-    let loadFile: MenuItem = ctrl "menuOpen"
-    let newFile: MenuItem = ctrl "menuNew"
+    let menuExit: MenuItem = ctrl "menuExit"
+    let menuSaveFile: MenuItem = ctrl "menuSave"
+    let menuLoadFile: MenuItem = ctrl "menuOpen"
+    let menuNewFile: MenuItem = ctrl "menuNew"
+    let menuAddSourceImage: MenuItem = ctrl "menuAddSourceImage"
+    let menuStartAnalysis: MenuItem = ctrl "menuStartAnalysis"
+    let menuHightlightRBC: MenuItem = ctrl "menuHightlightRBC"
 
-    let addSourceImage: MenuItem = ctrl "menuAddSourceImage"
     let txtPatient: TextBox = ctrl "txtPatient"
     let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia"
 
-    let butStartAnalysis: Button = ctrl "butStartAnalysis"
-
     let stackPreviews: StackPanel = ctrl "stackPreviews"
 
     let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage"
@@ -51,16 +51,23 @@ let run (defaultConfig: Config) =
     let stackRBC: StackPanel = ctrl "stackRBC"
 
     // Initializations.
+    menuHightlightRBC.IsChecked <- displayHealthy
 
     // Utils.
     let extractRBCPreview (img: Emgu.CV.Image<Emgu.CV.Structure.Bgr, byte>) (rbc: RBC) : Emgu.CV.Image<Emgu.CV.Structure.Bgr, byte> =
         let rbcWidth = rbc.size.Width
         let rbcHeight = rbc.size.Height
-        img.GetSubRect(System.Drawing.Rectangle(System.Drawing.Point(rbc.center.X - rbcWidth / 2. |> Utils.roundInt, rbc.center.Y - rbcHeight / 2. |> Utils.roundInt),
-                                                System.Drawing.Size(Utils.roundInt rbcWidth, Utils.roundInt rbcHeight)))
+        let x = rbc.center.X - rbcWidth / 2. |> Utils.roundInt
+        let y = rbc.center.Y - rbcHeight / 2. |> Utils.roundInt
+        let w = Utils.roundInt rbcWidth
+        let h = Utils.roundInt rbcHeight
+        //Utils.dprintfn "w: %A, h: %A, cx: %A, cy: %A, img.w: %A, img.h: %A" w h x y img.Width img.Height
+        img.GetSubRect(System.Drawing.Rectangle(System.Drawing.Point((if x < 0 then 0 else x), (if y < 0 then 0 else y)),
+                                                System.Drawing.Size((if x + w >= img.Width then img.Width - x else w),
+                                                                    (if y + h >= img.Height then img.Height - y else h))))
 
     let setRBCFrameStyle (rbc: RBC) (frame: Views.RBCFrame) =
-        frame.Opacity <- if rbc.setManually || rbc.infected then 1. else 0.
+        frame.Opacity <- if displayHealthy || rbc.setManually || rbc.infected then 1. else 0.
         let color = if rbc.infected then colorRBCInfected else colorRBCHealthy
         frame.manuallyAdded.Visibility <- if rbc.setManually then Visibility.Visible else Visibility.Hidden
         frame.manuallyAdded.Fill <- color
@@ -73,7 +80,7 @@ let run (defaultConfig: Config) =
         frame.Tag <- rbc
         setRBCFrameStyle rbc frame
         frame.border.StrokeThickness <- 1.
-        frame.lblRBCNumber.Content <- rbc.num
+        frame.txtRBCNumber.Text <- rbc.num.ToString()
         frame
 
     let highlightRBCFrame (frame: Views.RBCFrame) (highlight: bool) =
@@ -81,10 +88,10 @@ let run (defaultConfig: Config) =
         if highlight
         then
             frame.border.StrokeThickness <- 3.
-            if not rbc.infected && not rbc.setManually then frame.Opacity <- 1.
+            if not rbc.infected && not rbc.setManually && not displayHealthy then frame.Opacity <- 1.
         else
             frame.border.StrokeThickness <- 1.
-            if not rbc.infected && not rbc.setManually then frame.Opacity <- 0.
+            if not rbc.infected && not rbc.setManually && not displayHealthy then frame.Opacity <- 0.
 
     let zoomToRBC (rbc: RBC) =
         scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
@@ -96,20 +103,31 @@ let run (defaultConfig: Config) =
             ""
         else
             let percent = 100. * (float nbInfected) / (float nbTotal)
-            sprintf "%.2f %% (%d / %d)" percent nbInfected nbTotal
+            sprintf "%.1f %% (%d / %d)" percent nbInfected nbTotal
 
     let updateCurrentImageInformation () =
         match state.CurrentImage with
         | Some srcImg ->
             let parasitemiaStr = parasitemiaText (state.ImageParasitemia srcImg)
-            txtImageInformation.Text <- sprintf "Parasitemia: %s" parasitemiaStr
+            txtImageInformation.Inlines.Clear()
+            txtImageInformation.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
+            txtImageInformation.Inlines.Add(parasitemiaStr)
+            txtImageInformation.Inlines.Add(Documents.LineBreak())
+
+            txtImageInformation.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
+            txtImageInformation.Inlines.Add(Documents.Run(string (Utils.roundInt <| 2. * srcImg.rbcRadius)))
+            txtImageInformation.Inlines.Add(Documents.Run(" px"))
+            txtImageInformation.Inlines.Add(Documents.LineBreak())
+
+            txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
+            txtImageInformation.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
         | _ -> ()
 
     let updateGlobalParasitemia () =
         txtGlobalParasitemia.Text <- parasitemiaText state.GlobalParasitemia
 
     let rec setAsInfected (rbc: RBC) (infected: bool) =
-        rbc.SetAsInfected infected
+        state.SetAsInfected rbc infected
         canvasCurrentImage.Children
         |> Seq.cast<Views.RBCFrame>
         |> Seq.iter
@@ -144,14 +162,14 @@ let run (defaultConfig: Config) =
         match state.CurrentImage with
         | Some srcImg ->
             let mutable currentPreview = 0
-            for rbc in srcImg.rbcs |> List.filter (fun rbc -> rbc.infected) do
+            for rbc in srcImg.rbcs |> List.filter (fun rbc -> displayHealthy || rbc.infected) do
                 let previewInfected =
                     if currentPreview < stackRBC.Children.Count
                     then
                         RBCFrameFromExisting rbc (stackRBC.Children.[currentPreview] :?> Views.RBCFrame)
                     else
                         let f = RBCFrame rbc
-                        f.MouseLeftButtonUp.AddHandler(fun obj args -> zoomToRBC ((obj :?> Views.RBCFrame).Tag :?> RBC))
+                        f.MouseLeftButtonUp.AddHandler(fun obj args -> zoomToRBC (f.Tag :?> RBC))
                         stackRBC.Children.Add(f) |> ignore
                         f
 
@@ -187,81 +205,134 @@ let run (defaultConfig: Config) =
                 canvasCurrentImage.Children.[i].Visibility <- Visibility.Hidden
         | _ -> ()
 
-    // Operations.
-    let synchronizeState () =
-        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.))
+    let saveCurrentDocument () =
+        if state.FilePath = ""
+        then
+            let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter);
+            let res = dialog.ShowDialog()
+            if res.HasValue && res.Value
+            then
+                state.FilePath <- dialog.FileName
+                state.Save()
+        else
+            state.Save()
 
-        canvasCurrentImage.Height <- float srcImg.img.Height
-        canvasCurrentImage.Width <- float srcImg.img.Width
-        canvasCurrentImage.Background <- ImageBrush(BitmapSourceConvert.ToBitmapSource(srcImg.img))
+    // Ask the use to save the current document if neccessary.
+    let askSaveCurrent () =
+        if state.AlteredSinceLastSave
+        then
+            match MessageBox.Show("Would you like to save the current document?", "Saving the current document", MessageBoxButton.YesNo, MessageBoxImage.Question) with
+            | MessageBoxResult.Yes -> saveCurrentDocument ()
+            | _ -> ()
 
-        updateRBCFramesCurrent ()
-        updateRBCFramesPreview ()
-        updateCurrentImageInformation ()
+    let updateCurrentImage () =
+        match state.CurrentImage with
+        | Some srcImg ->
+            // Highlight the preview.
+            stackPreviews.Children
+            |> Seq.cast<Views.ImageSourcePreview>
+            |> Seq.iter (fun preview -> preview.border.BorderThickness <- Thickness(if preview.Tag = (srcImg :> Object) then 3. else 0.))
+
+            canvasCurrentImage.Height <- float srcImg.img.Height
+            canvasCurrentImage.Width <- float srcImg.img.Width
+            canvasCurrentImage.Background <- ImageBrush(BitmapSourceConvert.ToBitmapSource(srcImg.img))
+
+            updateRBCFramesCurrent ()
+            updateRBCFramesPreview ()
+            updateCurrentImageInformation ()
+        | None ->
+            stackRBC.Children.Clear()
+            canvasCurrentImage.Children.Clear()
+            canvasCurrentImage.Background <- Brushes.Black
 
+    let setCurrentImage (srcImg: SourceImage) =
+        if state.CurrentImage.IsNone || state.CurrentImage.Value <> srcImg
+        then
+            state.CurrentImage <- Some srcImg
+            updateCurrentImage ()
+
+    let updateViewportPreview () =
+        for preview in stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> do
+            let srcImg = preview.Tag :?> SourceImage
+            if Some srcImg = state.CurrentImage then
+                preview.viewport.Visibility <- Visibility.Visible
+
+                let canvasWidth = canvasCurrentImage.ActualWidth * currentScale
+                let canvasHeight = canvasCurrentImage.ActualHeight * currentScale
+                let previewWidth = (preview.ActualWidth - preview.BorderThickness.Left - preview.BorderThickness.Right)
+                let previewHeight = (preview.ActualHeight - preview.BorderThickness.Top - preview.BorderThickness.Bottom)
+
+                let marginLeft = previewWidth * (scrollViewCurrentImage.HorizontalOffset - borderCurrentImage.BorderThickness.Left) / canvasWidth - 2.
+                let marginRight = previewWidth * (canvasWidth - (scrollViewCurrentImage.HorizontalOffset - borderCurrentImage.BorderThickness.Right) - scrollViewCurrentImage.ViewportWidth) / canvasWidth - 2.
+                let marginTop = previewHeight * (scrollViewCurrentImage.VerticalOffset - borderCurrentImage.BorderThickness.Top) / canvasHeight - 2.
+                let marginBottom = previewHeight * (canvasHeight - (scrollViewCurrentImage.VerticalOffset - borderCurrentImage.BorderThickness.Bottom) - scrollViewCurrentImage.ViewportHeight) / canvasHeight - 2.
+
+                preview.viewport.Margin <-
+                    Thickness(
+                        marginLeft,
+                        marginTop,
+                        marginRight,
+                        marginBottom)
+            else
+                preview.viewport.Visibility <- Visibility.Hidden
 
     let addPreview (srcImg: SourceImage) =
         let imgCtrl = Views.ImageSourcePreview(Margin = Thickness(3.))
-        imgCtrl.lblImageNumber.Content <- srcImg.num
+
+        imgCtrl.menuRemoveImage.Click.AddHandler(fun obj args ->
+            stackPreviews.Children.Remove(imgCtrl)
+            let srcImg = imgCtrl.Tag :?> SourceImage
+            let currentRemoved = Some srcImg = state.CurrentImage
+            state.RemoveSourceImage srcImg
+            if currentRemoved
+            then
+                updateCurrentImage()
+            stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> |> Seq.iter (fun imgPreview -> imgPreview.txtImageNumber.Text <- (imgPreview.Tag :?> SourceImage).num.ToString()))
+
+        imgCtrl.Tag <- srcImg
+        imgCtrl.txtImageNumber.Text <- srcImg.num.ToString()
         let width = 200
         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)))
+        imgCtrl.MouseLeftButtonUp.AddHandler(fun obj args -> setCurrentImage (state.SourceImages |> Seq.find (fun srcImg -> (srcImg :> Object) = imgCtrl.Tag)))
 
     let updatePreviews () =
         stackPreviews.Children.Clear ()
-        if state.SourceImages.Count() > 0
-        then
-            for srcImg in state.SourceImages do
-                addPreview srcImg
-            match state.CurrentImage with
-            | Some srcImg -> setCurrentImage srcImg
-            | _ -> ()
+        for srcImg in state.SourceImages do
+            addPreview srcImg
+        updateCurrentImage ()
 
     let updateGUI () =
         txtPatient.Text <- state.PatientID
         updatePreviews ()
         updateGlobalParasitemia ()
 
-    exit.Click.AddHandler(fun obj args -> mainWindow.Root.Close())
-    saveFile.Click.AddHandler(fun obj args ->
-        synchronizeState ()
-        if state.FilePath = ""
-        then
-            let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter);
-            let res = dialog.ShowDialog()
-            if res.HasValue && res.Value
-            then
-                state.FilePath <- dialog.FileName
-                state.Save()
-        else
-            state.Save())
+    txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+
+    menuExit.Click.AddHandler(fun obj args ->
+        askSaveCurrent ()
+        mainWindow.Root.Close())
+
+    menuSaveFile.Click.AddHandler(fun obj args -> saveCurrentDocument ())
 
-    loadFile.Click.AddHandler(fun obj args ->
+    menuLoadFile.Click.AddHandler(fun obj args ->
         // TODO: if current state not saved and not empty, ask to save it.
         let dialog = OpenFileDialog(Filter = PiaZ.filter)
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
         then
+            askSaveCurrent ()
             state.FilePath <- dialog.FileName
             state.Load()
             updateGUI ())
 
-    newFile.Click.AddHandler(fun obj args ->
-        // TODO: if current state not saved and not empty, ask to save it.
+    menuNewFile.Click.AddHandler(fun obj args ->
+        askSaveCurrent ()
         state.Reset()
         updateGUI())
 
-    addSourceImage.Click.AddHandler(fun obj args ->
+    menuAddSourceImage.Click.AddHandler(fun obj args ->
         let dialog = OpenFileDialog(Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff")
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
@@ -271,18 +342,18 @@ let run (defaultConfig: Config) =
             updateGlobalParasitemia ()
             if state.SourceImages.Count() = 1
             then
-                setCurrentImage srcImg)
+                updateCurrentImage ())
 
-    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
-        updateGlobalParasitemia ()
+    menuStartAnalysis.Click.AddHandler(fun obj args ->
+        if Analysis.showWindow mainWindow.Root state defaultConfig
+        then
+            updateGlobalParasitemia ()
+            updateCurrentImage ())
 
-        // Refresh current image.
-        match state.CurrentImage with
-        | Some srcImg -> setCurrentImage srcImg
-        | _ -> ())
+    menuHightlightRBC.Click.AddHandler(fun obj args ->
+        displayHealthy <- menuHightlightRBC.IsChecked
+        updateRBCFramesPreview ()
+        updateRBCFramesCurrent ())
 
     // Zoom on the current image.
     let adjustCurrentImageBorders (deltaX: float) (deltaY: float) =
@@ -328,12 +399,12 @@ let run (defaultConfig: Config) =
             let centerX = scrollViewCurrentImage.HorizontalOffset + scrollViewCurrentImage.ViewportWidth / 2. - borderCurrentImage.BorderThickness.Left
             let centerY = scrollViewCurrentImage.VerticalOffset + scrollViewCurrentImage.ViewportHeight / 2. - borderCurrentImage.BorderThickness.Top
 
-            //canvasCurrentImage.Margin <- Thickness(canvasCurrentImage.Margin.Top * realScaleFactor)
             currentImageScaleTransform.ScaleX <- currentScale
             currentImageScaleTransform.ScaleY <- currentScale
 
             scrollViewCurrentImage.ScrollToHorizontalOffset(centerX * realScaleFactor - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
             scrollViewCurrentImage.ScrollToVerticalOffset(centerY * realScaleFactor - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
+
         args.Handled <- true)
 
     // Pan on the current image.
@@ -356,6 +427,7 @@ let run (defaultConfig: Config) =
             let deltaY = scrollStartPosition.Y - position.Y
             scrollViewCurrentImage.ScrollToHorizontalOffset(deltaX + scrollStartOffsetX)
             scrollViewCurrentImage.ScrollToVerticalOffset(deltaY + scrollStartOffsetY)
+
             args.Handled <- true)
 
     borderCurrentImage.PreviewMouseLeftButtonUp.AddHandler(fun obj args ->
@@ -365,6 +437,8 @@ let run (defaultConfig: Config) =
             borderCurrentImage.ReleaseMouseCapture()
             args.Handled <- true)
 
-    // display mainWindow img
+    // Viewport preview.
+    scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
+
     mainWindow.Root.Show()
     app.Run()
\ No newline at end of file
index 6fd7ba2..262c89d 100644 (file)
@@ -6,10 +6,16 @@
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       mc:Ignorable="d" d:DesignWidth="119.223" d:DesignHeight="84.911"
                >
-   <Border x:Name="border" BorderBrush="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}">
-      <Grid>
+   <Border x:Name="border" ClipToBounds="True" BorderBrush="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}">
+      <Grid x:Name="grid">
+         <Grid.ContextMenu>
+            <ContextMenu>
+               <MenuItem x:Name="menuRemoveImage" Header="_Remove image"  />
+            </ContextMenu>
+         </Grid.ContextMenu>
          <Image x:Name="imagePreview" />
-         <Label x:Name="lblImageNumber" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White"/>
+         <TextBlock x:Name="txtImageNumber" Padding="2" Text="42" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White" Margin="0,0,3,3"/>
+         <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5"/>
       </Grid>
    </Border>
 </UserControl>
\ No newline at end of file
index 9aecf1b..49203c6 100644 (file)
          <MenuItem Header="_Images">
             <MenuItem x:Name="menuAddSourceImage" Header="_Add a source image" />
          </MenuItem>
+         <MenuItem Header="_Analysis">
+            <MenuItem x:Name="menuStartAnalysis" Header="_Show analyses window" />
+         </MenuItem>
+         <MenuItem Header="_View">
+            <MenuItem x:Name="menuHightlightRBC" Header="_Highlight healthy erytrocytes" IsCheckable="True" />
+         </MenuItem>
       </Menu>
       <Grid x:Name="gridMain">
          <Grid.RowDefinitions>
             <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
-            <ColumnDefinition Width="130"/>
+            <ColumnDefinition Width="180"/>
             <ColumnDefinition/>
          </Grid.ColumnDefinitions>
          <Grid x:Name="gridGlobalInfo" Grid.ColumnSpan="2" Margin="3,3,3,3" >
             <Grid.ColumnDefinitions>
-               <ColumnDefinition Width="Auto"/>
+               <ColumnDefinition Width="101"/>
+               <ColumnDefinition Width="21"/>
                <ColumnDefinition/>
             </Grid.ColumnDefinitions>
             <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
             </Grid.RowDefinitions>
-            <Label x:Name="lblPatient" Margin="10, 3, 3, 3" Content="Patient ID"/>
-            <Label x:Name="lblGlobalParasitemia" Margin="10,3,3,3" Content="Global parasitemia" Grid.Row="1" />
-            <TextBox x:Name="txtPatient" Grid.Column="1" Margin="3, 3, 10, 3" TextWrapping="Wrap" VerticalAlignment="Center" />
-            <TextBox x:Name="txtGlobalParasitemia" Grid.Column="1" Grid.Row="2" Margin="3, 3, 10, 3" TextWrapping="Wrap" VerticalAlignment="Center" IsReadOnly="True" />
-         </Grid>
-         <Grid Grid.Row="1">
-            <Grid.RowDefinitions>
-               <RowDefinition/>
-               <RowDefinition Height="Auto" />
-            </Grid.RowDefinitions>
-            <Border BorderBrush="Black" BorderThickness="1"  Margin="3" Grid.Row="0" >
-               <ScrollViewer x:Name="scrollPreviews" VerticalScrollBarVisibility="Auto" >
-                  <StackPanel x:Name="stackPreviews" />
-               </ScrollViewer>
-            </Border>
-            <Button x:Name="butStartAnalysis" Content="Start analysis" Grid.Row="1" Margin="3"/>
+            <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" />
          </Grid>
+         <Border BorderBrush="Black" BorderThickness="1"  Margin="3" Grid.Row="1" >
+            <ScrollViewer x:Name="scrollPreviews" VerticalScrollBarVisibility="Auto" >
+               <StackPanel x:Name="stackPreviews" />
+            </ScrollViewer>
+         </Border>
          <Grid Grid.Column="2" Grid.Row="2">
             <Grid.RowDefinitions>
                <RowDefinition Height="100"/>
                <RowDefinition/>
-               <RowDefinition Height="60"/>
+               <RowDefinition Height="Auto"/>
             </Grid.RowDefinitions>
             <ScrollViewer x:Name="scrollViewCurrentImage" Grid.Row="1" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible" Background="Black" MinHeight="100" MinWidth="100">
                <Border x:Name="borderCurrentImage" BorderBrush="Transparent">
                   <Canvas x:Name="canvasCurrentImage" Height="100" Width="100" />
                </Border>
             </ScrollViewer>
-            <Border Grid.RowSpan="1"  Margin="3" >
-               <ScrollViewer x:Name="scrollRBC" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible">
-                  <StackPanel x:Name="stackRBC" Orientation="Horizontal" />
-               </ScrollViewer>
-            </Border>
-            <TextBlock x:Name="txtImageInformation" Grid.Row="2" TextWrapping="Wrap" />
+            <ScrollViewer x:Name="scrollRBC" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible" Grid.RowSpan="1"  Margin="3">
+               <StackPanel x:Name="stackRBC" Orientation="Horizontal" />
+            </ScrollViewer>
+            <TextBlock x:Name="txtImageInformation" Grid.Row="2" TextWrapping="Wrap" Margin="3" />
          </Grid>
       </Grid>
    </DockPanel>
index a373e53..e330a2d 100644 (file)
@@ -1,6 +1,7 @@
 // ParasitemIA Zipped file format.
 module Parasitemia.GUI.PiaZ
 
+open System
 open System.Windows
 open System.IO
 open System.IO.Compression
@@ -22,6 +23,8 @@ type FileData = {
 // The json type associated to a source image.
 type JSONSourceImage = JsonProvider<"""
     {
+        "RBCRadius" : 32.5,
+        "dateLastAnalysis" : 1.5,
         "rbcs": [
             {
                 "num": 1,
@@ -66,8 +69,10 @@ let save (filePath: string) (data: FileData) =
         imgSrc.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
 
         let imgJSON =
-            JSONSourceImage.Root([| for rbc in imgSrc.rbcs ->
-                                        JSONSourceImage.Rbc(
+            JSONSourceImage.Root(decimal imgSrc.rbcRadius,
+                                 decimal <| imgSrc.dateLastAnalysis.ToFileTimeUtc(),
+                                 [| 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,
@@ -94,9 +99,13 @@ let load (filePath: string) : FileData =
                 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);
-                                                              infectedArea = rbc.StainArea } ] } ]
+                yield { num = imgNum
+                        rbcRadius = float imgJSON.RbcRadius
+                        dateLastAnalysis = DateTime.FromFileTimeUtc(int64 imgJSON.DateLastAnalysis)
+                        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);
+                                      infectedArea = rbc.StainArea } ] } ]
     { sources = sources; patientID = mainJSON.PatientId }
\ No newline at end of file
index 2a56b00..1a904c1 100644 (file)
@@ -16,6 +16,6 @@
       <Rectangle x:Name="border" Fill="#00000000">
       </Rectangle>
       <Polygon x:Name="manuallyAdded" Points="0,0 12,0, 12,12" Fill="Black" HorizontalAlignment="Right" VerticalAlignment="Top" />
-      <Label x:Name="lblRBCNumber" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White"/>
+      <TextBlock x:Name="txtRBCNumber" Padding="2" Text="42" HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Foreground="White" Margin="0,0,3,3"/>
    </Grid>
 </UserControl>
\ No newline at end of file
index 44de2e3..44ca21b 100644 (file)
@@ -1,5 +1,6 @@
 module Parasitemia.GUI.State
 
+open System
 open System.Collections.Generic
 open System.Windows
 
@@ -10,10 +11,20 @@ open Types
 
 type State () =
     let sourceImages = List<SourceImage>()
+    let mutable alteredSinceLastSave = false
+    let mutable patientID = ""
 
+    member this.AlteredSinceLastSave = alteredSinceLastSave
     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.PatientID
+        with get () : string = patientID
+        and set id =
+            if id <> patientID
+            then
+                alteredSinceLastSave <- true
+                patientID <- id
 
     member this.ImageParasitemia (srcImg: SourceImage) : int * int =
         List.length srcImg.rbcs,
@@ -25,9 +36,18 @@ type State () =
                         let nb, nbInfected = this.ImageParasitemia srcImg
                         nbTotal + nb, nbTotalInfected + nbInfected) (0, 0)
 
+
+    member this.SetAsInfected (rbc: RBC) (infected: bool) =
+        if infected <> rbc.infected
+        then
+            alteredSinceLastSave <- true
+            rbc.infected <- infected
+            rbc.setManually <- not rbc.setManually
+
     member this.Save () =
         let data = { PiaZ.sources = List.ofSeq sourceImages; PiaZ.patientID = this.PatientID }
         PiaZ.save this.FilePath data
+        alteredSinceLastSave <- false
 
     member this.Load () =
         let data = PiaZ.load this.FilePath
@@ -36,32 +56,72 @@ type State () =
         sourceImages.InsertRange(0, data.sources)
         if sourceImages.Count > 0
         then this.CurrentImage <- Some sourceImages.[0]
+        alteredSinceLastSave <- false
 
     member this.AddSourceImage (filePath: string) : SourceImage =
-        let srcImg = { num = sourceImages.Count + 1; rbcs = []; img = new Image<Bgr, byte>(filePath) }
+        let srcImg = { num = sourceImages.Count + 1; rbcRadius = 0.; dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
         sourceImages.Add(srcImg)
         if sourceImages.Count = 1
         then this.CurrentImage <- Some sourceImages.[0]
+        alteredSinceLastSave <- true
         srcImg
 
-    member this.SetResult (imgNum: int) (cells: Cell list) =
+    member this.RemoveSourceImage (srcImg: SourceImage) =
+        let isCurrent =
+            match this.CurrentImage with
+            | Some srcImg' -> srcImg = srcImg'
+            | _ -> false
+
+        if sourceImages.Remove(srcImg)
+        then
+            alteredSinceLastSave <- true
+            if isCurrent
+            then
+                this.CurrentImage <- if sourceImages.Count > 0 then Some sourceImages.[0] else None
+        // Re-numbered the images.
+        sourceImages |> Seq.iteri (fun i srcImg -> srcImg.num <- i + 1)
+
+    member this.SetResult (imgNum: int) (rbcRadius: float) (cells: Cell list) =
         let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = imgNum)
+
         let w = sourceImage.img.Width
         let h = sourceImage.img.Height
+
+        sourceImage.rbcRadius <- rbcRadius
+        sourceImage.dateLastAnalysis <- DateTime.UtcNow
+
+        // To match with previously manually altered RBC.
+        let manuallyAlteredPreviousRBCS = sourceImage.rbcs |> List.filter (fun rbc -> rbc.setManually)
+        let tolerance = rbcRadius * 0.5 // +/-.
+        let getPreviousRBC (center: Point) : RBC option =
+            manuallyAlteredPreviousRBCS |> List.tryFind (fun rbc -> rbc.center.X > center.X - tolerance && rbc.center.X < center.X + tolerance &&
+                                                                    rbc.center.Y > center.Y - tolerance && rbc.center.Y < center.Y + tolerance)
+
         sourceImage.rbcs <- cells
             |> List.filter (fun cell -> match cell.cellClass with HealthyRBC | InfectedRBC -> true | _ -> false )
             |> List.sortByDescending (fun cell -> cell.infectedArea, (w - cell.center.X) + (h - cell.center.Y))
             |> List.mapi (fun i cell ->
+                let center = Point(float cell.center.X, float cell.center.Y)
+                let infected, setManually =
+                    match getPreviousRBC center with
+                    | Some rbc -> rbc.infected, true
+                    | _ -> cell.cellClass = InfectedRBC, false
+
                 { num = i + 1
-                  infected = cell.cellClass = InfectedRBC
-                  setManually = false
-                  center = Point(float cell.center.X, float cell.center.Y)
+                  infected = infected
+                  setManually = setManually
+                  center = center
                   size = Size(float cell.elements.Width, float cell.elements.Height)
                   infectedArea = cell.infectedArea })
 
+        alteredSinceLastSave <- true
+
     member this.SourceImages : SourceImage seq =
         sourceImages :> SourceImage seq
 
     member this.Reset () =
         this.PatientID <- ""
-        sourceImages.Clear()
\ No newline at end of file
+        this.FilePath <- ""
+        this.CurrentImage <- None
+        sourceImages.Clear()
+        alteredSinceLastSave <- false
\ No newline at end of file
index 6e00586..f22afdb 100644 (file)
@@ -1,5 +1,6 @@
 module Parasitemia.GUI.Types
 
+open System
 open System.Windows
 
 open Emgu.CV
@@ -13,14 +14,11 @@ type RBC = {
 
     center: Point
     size: Size
-    infectedArea: int } with
-    member this.SetAsInfected (infected: bool) =
-        if infected <> this.infected
-        then
-            this.infected <- infected
-            this.setManually <- not this.setManually
+    infectedArea: int }
 
 type SourceImage = {
-    num: int
+    mutable num: int
+    mutable rbcRadius: float
+    mutable dateLastAnalysis: DateTime // UTC.
     img: Image<Bgr, byte>
     mutable rbcs: RBC list }
\ No newline at end of file
index 5ee6886..2be9be1 100644 (file)
@@ -14,19 +14,26 @@ open Config
 open Types
 
 
-let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) : Cell list =
+let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportProgress: (int -> unit) option) : Cell list =
+    let inline report (percent: int) =
+        match reportProgress with
+        | Some f -> f percent
+        | _ -> ()
+
     use green = img.Item(1)
     let greenFloat = green.Convert<Gray, float32>()
     let filteredGreen = gaussianFilter greenFloat (float config.Parameters.preFilterSigma)
 
     logTime "areaOpen 1" (fun () -> ImgTools.areaOpenF filteredGreen config.Parameters.initialAreaOpen)
+    report 8
 
     let r1 = logTime "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) (10, 80) 0.5 |> float32)
     // let r2 = logTime "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen (10, 80) |> float32)
     // log (sprintf "r1: %A, r2: %A" r1 r2)
     config.RBCRadius <- r1
+    report 24
 
-    let secondAreaOpen = int <| config.RBCArea / 3.f
+    let secondAreaOpen = int <| config.RBCArea * config.Parameters.ratioSecondAreaOpen
 
     if secondAreaOpen > config.Parameters.initialAreaOpen
     then
@@ -41,8 +48,10 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) : Cell li
     let allEllipses, ellipses = logTime "Finding ellipses" (fun () ->
         let matchingEllipses = Ellipse.find edges xGradient yGradient config
         matchingEllipses.Ellipses, matchingEllipses.PrunedEllipses)
+    report 80
 
     let cells = logTime "Classifier" (fun () -> Classifier.findCells ellipses parasites filteredGreenWhitoutStain config)
+    report 100
 
     // Output pictures if debug flag is set.
     match config.Debug with
@@ -95,11 +104,28 @@ 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 =
+// ID * cell radius * cell list.
+let doMultipleAnalysis (imgs: (string * Image<Bgr, byte>) list) (config : Config) (reportProgress: (int -> unit) option) : (string * float * Cell list) list =
+    let inline report (percent: int) =
+        match reportProgress with
+        | Some f -> f percent
+        | _ -> ()
+
+    let monitor = Object()
+    let mutable total = 0
+    let nbImgs = List.length imgs
+    let reportProgressImg =
+        (fun progress ->
+            lock monitor (fun () -> total <- total + progress)
+            report (total / nbImgs))
+
     let nbConcurrentTaskLimit = 4
     let n = Environment.ProcessorCount
 
     imgs
-    |> PSeq.map (fun (id, img) -> id, doAnalysis img id (config.Copy()))
+    |> PSeq.map (fun (id, img) ->
+        let localConfig = config.Copy()
+        let cells = doAnalysis img id localConfig (Some reportProgressImg)
+        id, float localConfig.RBCRadius, cells)
     |> PSeq.withDegreeOfParallelism (if n > nbConcurrentTaskLimit then nbConcurrentTaskLimit else n)
-    |> PSeq.toList
\ No newline at end of file
+    |> PSeq.toList
index b3a7e59..865840f 100644 (file)
     <Compile Include="GUI\ImageSourcePreview.xaml.fs" />
     <Resource Include="GUI\RBCFrame.xaml" />
     <Compile Include="GUI\RBCFrame.xaml.fs" />
+    <Resource Include="GUI\AnalysisWindow.xaml" />
+    <Compile Include="GUI\AnalysisWindow.xaml.fs" />
     <Resource Include="GUI\MainWindow.xaml" />
     <Compile Include="GUI\MainWindow.xaml.fs" />
     <Compile Include="GUI\Types.fs" />
     <Compile Include="GUI\PiaZ.fs" />
     <Compile Include="GUI\State.fs" />
+    <Compile Include="GUI\Analysis.fs" />
     <Compile Include="GUI\GUI.fs" />
     <Compile Include="Program.fs" />
     <None Include="App.config" />
     <Resource Include="Resources\logo_256.png" />
   </ItemGroup>
   <ItemGroup>
+    <Reference Include="Castle.Core">
+      <HintPath>..\packages\Castle.Core.3.3.3\lib\net45\Castle.Core.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="Emgu.CV">
       <HintPath>..\..\..\Emgu\emgucv-windows-universal 3.0.0.2157\bin\Emgu.CV.dll</HintPath>
     </Reference>
       <HintPath>..\packages\FSharp.Collections.ParallelSeq.1.0.2\lib\net40\FSharp.Collections.ParallelSeq.dll</HintPath>
       <Private>True</Private>
     </Reference>
-    <Reference Include="FSharp.Core">
+    <Reference Include="FSharp.Core, Version=$(TargetFSharpCoreVersion), Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
       <HintPath>..\packages\FSharp.Core.4.0.0.1\lib\net40\FSharp.Core.dll</HintPath>
       <Private>True</Private>
     </Reference>
       <HintPath>..\packages\FSharp.Data.2.2.5\lib\net40\FSharp.Data.dll</HintPath>
       <Private>True</Private>
     </Reference>
+    <Reference Include="FSharp.Data.TypeProviders" />
     <Reference Include="FSharp.ViewModule">
       <HintPath>..\packages\FSharp.ViewModule.Core.0.9.9.2\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\FSharp.ViewModule.dll</HintPath>
       <Private>True</Private>
       <Private>True</Private>
     </Reference>
     <Reference Include="log4net">
-      <HintPath>..\packages\log4net.1.2.10\lib\2.0\log4net.dll</HintPath>
+      <HintPath>..\packages\log4net.2.0.5\lib\net45-full\log4net.dll</HintPath>
       <Private>True</Private>
     </Reference>
     <Reference Include="MathNet.Numerics">
     <Reference Include="System.Core" />
     <Reference Include="System.Data" />
     <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="System.Data.Linq" />
     <Reference Include="System.Drawing" />
     <Reference Include="System.IO.Compression" />
     <Reference Include="System.IO.Compression.FileSystem" />
     </ProjectReference>
   </ItemGroup>
   <PropertyGroup>
-    <PostBuildEvent>xcopy "D:\Emgu\emgucv-windows-universal 3.0.0.2157\bin\x64\*" "$(TargetDir)" /Y /D</PostBuildEvent>
+    <PostBuildEvent>xcopy "D:\Emgu\emgucv-windows-universal 3.0.0.2157\bin\x64" "$(TargetDir)x64" /Y /D /I</PostBuildEvent>
   </PropertyGroup>
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
index 720488d..fcb71da 100644 (file)
@@ -56,12 +56,12 @@ let find (filteredGreen: Image<Gray, float32>) (config: Config.Config) : Result
     let filteredGreenWithoutStain = filteredGreenWithoutInfection.Copy()
     ImgTools.areaCloseF filteredGreenWithoutStain (int config.StainArea)
 
-    // We use the filtered image to find the dark stain.
-    let _, mean_fg, mean_bg =
-        let hist = ImgTools.histogramImg filteredGreenWithoutInfection 300
-        ImgTools.otsu hist
-
-    let darkStain = filteredGreenWithoutInfection.Cmp(-(float mean_bg) * config.Parameters.darkStainLevel + (float mean_fg), CvEnum.CmpType.LessThan)
+    let darkStain =
+        // We use the filtered image to find the dark stain.
+        let _, mean_fg, mean_bg =
+            let hist = ImgTools.histogramImg filteredGreenWithoutInfection 300
+            ImgTools.otsu hist
+        filteredGreenWithoutInfection.Cmp(-(float mean_bg) * config.Parameters.darkStainLevel + (float mean_fg), CvEnum.CmpType.LessThan)
 
     let marker (img: Image<Gray, float32>) (closed: Image<Gray, float32>) (level: float) : Image<Gray, byte> =
         let diff = img.Copy()
@@ -73,6 +73,11 @@ let find (filteredGreen: Image<Gray, float32>) (config: Config.Config) : Result
     let infectionMarker = marker filteredGreen filteredGreenWithoutInfection config.Parameters.infectionLevel
     let stainMarker = marker filteredGreenWithoutInfection filteredGreenWithoutStain config.Parameters.stainLevel
 
+    // TODO: comprendre pourquoi des valeurs sont negatives!?!?
+    let blackTopHat = filteredGreen.CopyBlank()
+    CvInvoke.Subtract(filteredGreenWithoutInfection, filteredGreen, blackTopHat)
+    ImgTools.saveImg (ImgTools.normalizeAndConvert blackTopHat) "BottomHat.png"
+
     { darkStain = darkStain
       infection = infectionMarker
       stain = stainMarker },
index 711ec6f..038f827 100644 (file)
@@ -46,6 +46,7 @@ let main args =
             Config(
               {
                 initialAreaOpen = 2000
+                ratioSecondAreaOpen = 1.f / 3.f
 
                 minRbcRadius = -0.3f
                 maxRbcRadius = 0.3f
@@ -65,7 +66,7 @@ let main args =
                 maxStainRatio = 0.12 // 12 %
 
                 standardDeviationMaxRatio = 0.5 // 0.55
-                minimumCellArea = 0.5f })
+                minimumCellAreaFactor = 0.5f })
 
         match mode with
         | CmdLine (input, output) ->
@@ -90,9 +91,9 @@ let main args =
 
 
             Utils.logTime "Whole analyze" (fun () ->
-                let results = ImageAnalysis.doMultipleAnalysis images config
+                let results = ImageAnalysis.doMultipleAnalysis images config None
 
-                for id, cells in results do
+                for id, _, cells in results do
                     let total, infected = Utils.countCells cells
                     fprintf resultFile "File: %s %d %d %.2f\n" id total infected (100. * (float infected) / (float total)))
 
index b30e9ac..c2440b9 100644 (file)
@@ -17,7 +17,7 @@ let logTime (m: string) (f: unit -> 'a) : 'a =
     sw.Start()
     let res = f ()
     sw.Stop()
-    log <| sprintf "%A (time: %A ms)" m sw.ElapsedMilliseconds
+    log <| sprintf "%s (time: %d ms)" m sw.ElapsedMilliseconds
     res
 
 let inline lineFromTwoPoints (p1: PointD) (p2: PointD) : Line =
index da70217..b3965a3 100644 (file)
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="Castle.Core" version="1.1.0" targetFramework="net46" />
+  <package id="Castle.Core" version="3.3.3" targetFramework="net461" />
   <package id="Expression.Blend.Sdk" version="1.0.2" targetFramework="net46" />
   <package id="FSharp.Collections.ParallelSeq" version="1.0.2" targetFramework="net461" />
   <package id="FSharp.Core" version="4.0.0.1" targetFramework="net461" />
   <package id="FSharp.Data" version="2.2.5" targetFramework="net461" />
   <package id="FSharp.ViewModule.Core" version="0.9.9.2" targetFramework="net461" />
   <package id="FsXaml.Wpf" version="0.9.9" targetFramework="net46" />
-  <package id="log4net" version="1.2.10" targetFramework="net46" />
+  <package id="log4net" version="2.0.5" targetFramework="net461" />
   <package id="MathNet.Numerics" version="3.10.0" targetFramework="net461" />
   <package id="MathNet.Numerics.FSharp" version="3.10.0" targetFramework="net461" />
 </packages>
\ No newline at end of file
index 77830ee..270ffe1 100644 (file)
@@ -20,7 +20,7 @@
     <DefineConstants>DEBUG;TRACE</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
-    <UseVSHostingProcess>true</UseVSHostingProcess>
+    <UseVSHostingProcess>false</UseVSHostingProcess>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>pdbonly</DebugType>