The images to be analyzed can be selected.
authorGreg Burri <greg.burri@gmail.com>
Thu, 14 Jan 2016 18:00:23 +0000 (19:00 +0100)
committerGreg Burri <greg.burri@gmail.com>
Thu, 14 Jan 2016 18:00:23 +0000 (19:00 +0100)
17 files changed:
Parasitemia/Parasitemia/Config.fs
Parasitemia/Parasitemia/GUI/Analysis.fs
Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml
Parasitemia/Parasitemia/GUI/GUI.fs
Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml
Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml.fs [new file with mode: 0644]
Parasitemia/Parasitemia/GUI/MainWindow.xaml
Parasitemia/Parasitemia/GUI/PiaZ.fs
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/UnitsOfMeasure.fs
Parasitemia/Parasitemia/packages.config

index 9c23d83..e28398e 100644 (file)
@@ -30,14 +30,14 @@ type Parameters = {
     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 // When a cell must own less than this ratio to be a RBC.
+    stainSensitivity: float // between 0 (the least sensitive) and 1 (the most sensitive).
+    maxStainRatio: float // 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
+    infectionSensitivity: float // between 0 (the least sensitive) and 1 (the most sensitive).
 
     standardDeviationMaxRatio: float // The standard deviation of the pixel values of a cell can't be greater than standardDeviationMaxRatio * global standard deviation
-    minimumCellAreaFactor: float32 // Factor of the mean RBC area.
+    minimumCellAreaFactor: float32 // Factor of the mean RBC area. A cell with an area below this will be rejected.
 }
 
 let defaultParameters = {
@@ -59,39 +59,49 @@ let defaultParameters = {
     maxDarkStainRatio = 0.1 // 10 %
 
     infectionArea = 0.012f // 1.2 %
-    infectionLevel = 1.12 // Lower -> more sensitive.
+    infectionSensitivity = 0.9
 
     stainArea = 0.08f // 8 %
-    stainLevel = 1.1 // Lower -> more sensitive.
+    stainSensitivity = 0.9
     maxStainRatio = 0.12 // 12 %
 
     standardDeviationMaxRatio = 0.5 // 0.5
     minimumCellAreaFactor = 0.4f }
 
 type Config (param: Parameters) =
+    let mutable parameters: Parameters = param
+
     let initialRBCRadius : float32 =
-        let rbcRadiusInch: float<inch> = μm.ConvertToInch(param.rbcDiameter) / 2.
-        let rbcRadiusPx: float<px> = param.resolution * rbcRadiusInch
+        let rbcRadiusInch: float<inch> = (μmToInch parameters.rbcDiameter) / 2.
+        let rbcRadiusPx: float<px> = parameters.resolution * rbcRadiusInch
         float32 rbcRadiusPx
 
-    member this.Parameters = param
+    new () = Config(defaultParameters)
+
+    member this.Parameters with get() = parameters and set(param) = parameters <- param
     member val Debug = DebugOff with get, set
 
     member this.LPFStandardDeviation =
-        let stdDeviation: float<px> = μm.ConvertToInch(param.LPFStandardDeviation) * param.resolution
+        let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviation) * param.resolution
         float stdDeviation
 
     // Mean RBC radius.
     member val RBCRadius : float32 = initialRBCRadius with get, set
 
-    member this.RBCMinRadius = this.RBCRadius + param.minRbcRadius * this.RBCRadius
-    member this.RBCMaxRadius = this.RBCRadius + param.maxRbcRadius * this.RBCRadius
+    member this.RBCRadiusμm : float<μm> =
+        1.<px> * (float this.RBCRadius) / parameters.resolution |> inchToμm
+
+    member this.RBCMinRadius = this.RBCRadius + parameters.minRbcRadius * this.RBCRadius
+    member this.RBCMaxRadius = this.RBCRadius + parameters.maxRbcRadius * this.RBCRadius
 
     member this.RBCArea = PI * this.RBCRadius ** 2.f
-    member this.RBCMinArea = param.minimumCellAreaFactor * this.RBCArea
+    member this.RBCMinArea = parameters.minimumCellAreaFactor * this.RBCArea
+
+    member this.InfectionArea = parameters.infectionArea * this.RBCArea
+    member this.StainArea = parameters.stainArea * this.RBCArea
 
-    member this.InfectionArea = param.infectionArea * this.RBCArea
-    member this.StainArea = param.stainArea * this.RBCArea
+    member this.FormattedRadius =
+        sprintf "%d px (%.1f μm)" (Utils.roundInt <| 2.f * this.RBCRadius) (2. * this.RBCRadiusμm)
 
     member this.Copy () =
         this.MemberwiseClone() :?> Config
index acb9ce2..ccca1b4 100644 (file)
@@ -1,15 +1,23 @@
 module Parasitemia.GUI.Analysis
 
 open System
+open System.IO
+open System.Linq
 open System.Windows
 open System.Windows.Media
 open System.Windows.Markup
 open System.Windows.Shapes
 open System.Windows.Controls
+open System.Diagnostics
+open Microsoft.Win32 // For the common dialogs.
 
+open Emgu.CV.WPF
+
+open UnitsOfMeasure
 open Config
+open Types
 
-let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : bool =
+let showWindow (parent: Window) (state: State.State) : bool =
     let window = Views.AnalysisWindow()
     window.Root.Owner <- parent
     window.Root.Left <- parent.Left + parent.ActualWidth / 2. - window.Root.Width / 2.
@@ -21,6 +29,7 @@ let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : b
     let butClose: Button = ctrl "butClose"
     let butStart: Button = ctrl "butStart"
 
+    let stackImagesSourceSelection: StackPanel = ctrl "stackImagesSourceSelection"
     let progressBar: ProgressBar = ctrl "progress"
     let textLog: TextBlock = ctrl "textLog"
     let scrollLog: ScrollViewer = ctrl "scrollLog"
@@ -30,36 +39,94 @@ let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : b
         textLog.Inlines.Add(Documents.LineBreak())
         scrollLog.ScrollToBottom()))
 
+    let minPPI = 1.
+    let maxPPI = 10e6
+    let parseAndValidatePPI (input: string) : float option =
+        let res = ref 0.
+        if Double.TryParse(input, res) && !res >= minPPI && !res <= maxPPI
+        then Some !res
+        else None
+
     let monitor = Object()
+    let mutable atLeastOneAnalysisPerformed = false
     let mutable analysisPerformed = false
     let mutable analysisCancelled = false
 
+    let updateSourceImages () =
+        stackImagesSourceSelection.Children.Clear()
+        let width = int stackImagesSourceSelection.ActualWidth
+        for srcImg in state.SourceImages do
+            let imageSourceSelection = Views.ImageSourceSelection(Tag = srcImg, Margin = Thickness(3.))
+
+            let updateResolution () =
+                match parseAndValidatePPI imageSourceSelection.txtResolution.Text with
+                | Some resolution -> srcImg.config.Parameters <- { srcImg.config.Parameters with resolution = resolution * 1.<ppi> }
+                | None -> ()
+
+            imageSourceSelection.txtImageNumber.Text <- srcImg.num.ToString()
+            let height = srcImg.img.Height * width / srcImg.img.Width
+            imageSourceSelection.imagePreview.Source <- BitmapSourceConvert.ToBitmapSource(srcImg.img.Resize(width, height, Emgu.CV.CvEnum.Inter.Cubic))
+            imageSourceSelection.chkSelection.IsChecked <- Nullable<bool>(srcImg.dateLastAnalysis.Ticks = 0L)
+            imageSourceSelection.lblDateLastAnalysis.Content <- if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToString()
+
+            imageSourceSelection.txtResolution.Text <- srcImg.config.Parameters.resolution.ToString()
+            imageSourceSelection.menuZoom50X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "200000"; updateResolution ())
+            imageSourceSelection.menuZoom100X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "400000"; updateResolution ())
+
+            imageSourceSelection.txtResolution.PreviewTextInput.AddHandler(fun obj args ->
+                let text = imageSourceSelection.txtResolution.Text + args.Text
+                args.Handled <- match parseAndValidatePPI text with Some _ -> false | None -> true)
+
+            imageSourceSelection.imagePreview.MouseLeftButtonDown.AddHandler(fun obj args ->
+                let checkbox = imageSourceSelection.chkSelection
+                checkbox.IsChecked <- Nullable<bool>(not (checkbox.IsChecked.HasValue && checkbox.IsChecked.Value)))
+
+            imageSourceSelection.txtResolution.LostFocus.AddHandler(fun obj args -> updateResolution ())
+
+            stackImagesSourceSelection.Children.Add(imageSourceSelection) |> ignore
+
     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)
+        let imagesToProcess = [
+            for imageSelection in stackImagesSourceSelection.Children |> Seq.cast<Views.ImageSourceSelection> do
+                let chk = imageSelection.chkSelection.IsChecked
+                if chk.HasValue && chk.Value
+                then
+                    let srcImg = imageSelection.Tag :?> SourceImage
+                    yield srcImg.num.ToString(), srcImg.config, srcImg.img ]
+
+        if imagesToProcess.IsEmpty
+        then
+            MessageBox.Show("No image selected", "Cannot start analysis", MessageBoxButton.OK, MessageBoxImage.Information) |> ignore
+        else
+            analysisPerformed <- false
+            butStart.IsEnabled <- false
+            butClose.Content <- "Abort"
+            async {
+                let results =
+                    ImageAnalysis.doMultipleAnalysis
+                        imagesToProcess
+                        (Some (fun progress -> window.Root.Dispatcher.Invoke(fun () -> progressBar.Value <- float progress)))
+
+                lock monitor (
+                    fun() ->
+                        if not analysisCancelled
+                        then
+                            for id, cells in results do
+                                state.SetResult (int id) cells
+
+                            window.Root.Dispatcher.Invoke(fun () ->
+                                butStart.IsEnabled <- true
+                                butClose.Content <- "Close"
+                                updateSourceImages ())
+
+                            Utils.log "Analysis terminated successfully"
+                            atLeastOneAnalysisPerformed <- true
+                            analysisPerformed <- true)
+            } |> Async.Start)
+
+    window.Root.Loaded.AddHandler(fun obj args -> updateSourceImages ())
 
     window.Root.ShowDialog() |> ignore
 
@@ -67,7 +134,7 @@ let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : b
         if not analysisPerformed
             then
                 analysisCancelled <- true
-        analysisPerformed)
+        atLeastOneAnalysisPerformed)
 
 
     (*let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig
index 6da0675..7684ad9 100644 (file)
@@ -3,16 +3,16 @@
         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">
+        x:Name="AnalysisWindow" Height="453" Width="515" MinHeight="100" MinWidth="100" Title="Analysis" Icon="pack://application:,,,/Resources/logo_256.png">
    <Grid>
       <Grid.RowDefinitions>
-         <RowDefinition Height="100"/>
+         <RowDefinition Height="50*"/>
          <RowDefinition Height="30"/>
-         <RowDefinition/>
+         <RowDefinition Height="20*"/>
          <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 x:Name="scrollImagesSourceSelection" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Grid.Row="0" Margin="3" >
+         <StackPanel x:Name="stackImagesSourceSelection" />
       </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">
index a7bed54..b0af4cc 100644 (file)
@@ -114,8 +114,7 @@ let run (defaultConfig: Config) =
             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.Run(srcImg.config.FormattedRadius))
             txtImageInformation.Inlines.Add(Documents.LineBreak())
 
             txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
@@ -324,7 +323,6 @@ let run (defaultConfig: Config) =
             then
                 imgCtrl.ReleaseMouseCapture())
 
-
     let updatePreviews () =
         stackPreviews.Children.Clear ()
         for srcImg in state.SourceImages do
@@ -336,7 +334,7 @@ let run (defaultConfig: Config) =
         updatePreviews ()
         updateGlobalParasitemia ()
 
-    txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+    txtPatient.LostFocus.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
 
     menuExit.Click.AddHandler(fun obj args ->
         askSaveCurrent ()
@@ -365,7 +363,7 @@ let run (defaultConfig: Config) =
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
         then
-            let srcImg = state.AddSourceImage(dialog.FileName)
+            let srcImg = state.AddSourceImage dialog.FileName defaultConfig
             addPreview srcImg
             updateGlobalParasitemia ()
             if state.SourceImages.Count() = 1
@@ -373,7 +371,7 @@ let run (defaultConfig: Config) =
                 updateCurrentImage ())
 
     menuStartAnalysis.Click.AddHandler(fun obj args ->
-        if Analysis.showWindow mainWindow.Root state defaultConfig
+        if Analysis.showWindow mainWindow.Root state
         then
             updateGlobalParasitemia ()
             updateCurrentImage ())
index 1064059..ae8f5dd 100644 (file)
@@ -17,7 +17,7 @@
          <Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Margin="0,0,3,3" CornerRadius="5" >
             <TextBlock x:Name="txtImageNumber" Padding="2" Text="42" Foreground="White" />
          </Border>
-         <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5"/>
+         <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5" Visibility="Hidden"/>
       </Grid>
    </Border>
 </UserControl>
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml b/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml
new file mode 100644 (file)
index 0000000..4fcde66
--- /dev/null
@@ -0,0 +1,72 @@
+<UserControl
+               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:fsxaml="clr-namespace:FsXaml;assembly=FsXaml.Wpf"
+      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+      mc:Ignorable="d" d:DesignWidth="349.723" d:DesignHeight="118.911"
+               >
+   <UserControl.Background>
+      <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}"/>
+   </UserControl.Background>
+   <Grid x:Name="gridMain">
+      <Grid.ColumnDefinitions>
+         <ColumnDefinition Width="100"/>
+         <ColumnDefinition/>
+      </Grid.ColumnDefinitions>
+      <Grid x:Name="gridImage" Grid.ColumnSpan="1" VerticalAlignment="Top">
+         <Image x:Name="imagePreview" />
+         <CheckBox x:Name="chkSelection" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="3,3,0,0"/>
+         <Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Margin="0,0,3,3" CornerRadius="5" >
+            <TextBlock x:Name="txtImageNumber" Padding="2" Text="42" Foreground="White" />
+         </Border>
+         <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5" Visibility="Hidden"/>
+      </Grid>
+      <Grid Grid.Column="1">
+         <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="Auto"/>
+            <ColumnDefinition/>
+         </Grid.ColumnDefinitions>
+         <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="1*"/>
+         </Grid.RowDefinitions>
+         <Label Content="Last analysis" Grid.Column="0" Grid.Row="0" Margin="10,0,3,0" />
+         <Label Content="Resolution [PPI]" Grid.Column="0" Grid.Row="1" Margin="10,0,3,0" />
+         <Label x:Name="lblDateLastAnalysis" Grid.Column="1" Margin="3,0,3,0"/>
+         <Grid Grid.Column="1" Grid.Row="1">
+            <Grid.ColumnDefinitions>
+               <ColumnDefinition/>
+               <ColumnDefinition Width="Auto"/>
+            </Grid.ColumnDefinitions>
+            <TextBox x:Name="txtResolution" Margin="3" Text="" Grid.Column="0" />
+            <Button x:Name="butDefaultResolutions" Content="Defaults" Grid.Column="1" Margin="3">
+               <Button.ContextMenu>
+                  <ContextMenu>
+                     <MenuItem x:Name="menuZoom50X" Header="_200'000 PPI (50X)"  />
+                     <MenuItem x:Name="menuZoom100X" Header="_400'000 PPI (100X)" />
+                  </ContextMenu>
+               </Button.ContextMenu>
+               <Button.Style>
+                  <Style TargetType="{x:Type Button}">
+                     <Style.Triggers>
+                        <EventTrigger RoutedEvent="Click">
+                           <EventTrigger.Actions>
+                              <BeginStoryboard>
+                                 <Storyboard>
+                                    <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
+                                       <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
+                                    </BooleanAnimationUsingKeyFrames>
+                                 </Storyboard>
+                              </BeginStoryboard>
+                           </EventTrigger.Actions>
+                        </EventTrigger>
+                     </Style.Triggers>
+                  </Style>
+               </Button.Style>
+            </Button>
+         </Grid>
+      </Grid>
+   </Grid>
+</UserControl>
\ No newline at end of file
diff --git a/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml.fs b/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml.fs
new file mode 100644 (file)
index 0000000..1ca5c66
--- /dev/null
@@ -0,0 +1,17 @@
+namespace Parasitemia.GUI.Views
+
+open System
+open System.Windows
+open System.Windows.Data
+open System.Windows.Input
+
+open FSharp.ViewModule
+open FsXaml
+
+type ImageSourceSelection = XAML<"GUI/ImageSourceSelection.xaml", true>
+
+(* type ImageSourcePreviewController() =
+    inherit UserControlViewController<ImageSourcePreview>() *)
+
+(* type ImageSourcePreviewViewModel() =
+    inherit ViewModelBase() *)
index b24b48c..9fa8316 100644 (file)
@@ -42,8 +42,8 @@
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
             </Grid.RowDefinitions>
-            <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" />
+            <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>
index e330a2d..22359e9 100644 (file)
@@ -6,48 +6,36 @@ open System.Windows
 open System.IO
 open System.IO.Compression
 
-open FSharp.Data
-
 open Emgu.CV
 open Emgu.CV.Structure
 
+open Newtonsoft.Json
+open Newtonsoft.Json.Converters
+
 open Types
 
 let extension = ".piaz"
 let filter = "PIA|*.piaz"
 
+// Information associated to a document.
+type JSONInformation = {
+    patientID: string
+}
+
+// Information associated to each images.
+type JSONSourceImage = {
+    num: int
+    parameters: Config.Parameters
+    dateLastAnalysis: DateTime
+    rbcs: RBC List
+}
+
 type FileData = {
-    sources: SourceImage list
-    patientID: string }
-
-// The json type associated to a source image.
-type JSONSourceImage = JsonProvider<"""
-    {
-        "RBCRadius" : 32.5,
-        "dateLastAnalysis" : 1.5,
-        "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"
+    patientID: string
+    images: SourceImage list
+}
+
+let mainEntryName = "info.json"
 let imageExtension = ".tiff"
 
 let save (filePath: string) (data: FileData) =
@@ -57,55 +45,40 @@ let save (filePath: string) (data: FileData) =
         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)
+    let mainEntry = file.CreateEntry(mainEntryName, CompressionLevel.Fastest)
+    use mainEntryWriter = new StreamWriter(mainEntry.Open())
+    mainEntryWriter.Write(JsonConvert.SerializeObject({ JSONInformation.patientID = data.patientID }))
 
     // Write each images and the associated information.
-    for imgSrc in data.sources do
-        let imgFilename = (string imgSrc.num) + imageExtension
+    for srcImg in data.images do
+        let imgFilename = (string srcImg.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(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,
-                                            rbc.infectedArea) |])
+        srcImg.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
 
         let imgJSONEntry = file.CreateEntry(imgFilename + ".json", CompressionLevel.Fastest)
         use imgJSONFileWriter = new StreamWriter(imgJSONEntry.Open())
-        imgJSON.JsonValue.WriteTo(imgJSONFileWriter, JsonSaveOptions.None)
+        imgJSONFileWriter.Write(JsonConvert.SerializeObject({ num = srcImg.num; parameters = srcImg.config.Parameters; dateLastAnalysis = srcImg.dateLastAnalysis; rbcs = srcImg.rbcs }))
 
 
 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
-                        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
+    let mainEntry = file.GetEntry(mainEntryName)
+    use mainEntryReader = new StreamReader(mainEntry.Open())
+    let info = JsonConvert.DeserializeObject<JSONInformation>(mainEntryReader.ReadToEnd())
+
+    { patientID = info.patientID
+      images = [ let mutable imgNum = 0
+                 for imgEntry in file.Entries do
+                    if imgEntry.Name.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 imgEntry = file.GetEntry(imgEntry.Name + ".json")
+                        use imgEntryFileReader = new StreamReader(imgEntry.Open())
+                        let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage>(imgEntryFileReader.ReadToEnd())
+                        yield { num = imgNum
+                                config = Config.Config(imgInfo.parameters)
+                                dateLastAnalysis = imgInfo.dateLastAnalysis
+                                img = img
+                                rbcs = imgInfo.rbcs } ] }
\ No newline at end of file
index 44ca21b..3b9d11e 100644 (file)
@@ -45,7 +45,7 @@ type State () =
             rbc.setManually <- not rbc.setManually
 
     member this.Save () =
-        let data = { PiaZ.sources = List.ofSeq sourceImages; PiaZ.patientID = this.PatientID }
+        let data = { PiaZ.FileData.patientID = this.PatientID; PiaZ.FileData.images = List.ofSeq sourceImages }
         PiaZ.save this.FilePath data
         alteredSinceLastSave <- false
 
@@ -53,13 +53,13 @@ type State () =
         let data = PiaZ.load this.FilePath
         this.PatientID <- data.patientID
         sourceImages.Clear()
-        sourceImages.InsertRange(0, data.sources)
+        sourceImages.InsertRange(0, data.images)
         if sourceImages.Count > 0
         then this.CurrentImage <- Some sourceImages.[0]
         alteredSinceLastSave <- false
 
-    member this.AddSourceImage (filePath: string) : SourceImage =
-        let srcImg = { num = sourceImages.Count + 1; rbcRadius = 0.; dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
+    member this.AddSourceImage (filePath: string) (defaultConfig: Config.Config) : SourceImage =
+        let srcImg = { num = sourceImages.Count + 1; config = defaultConfig.Copy(); dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
         sourceImages.Add(srcImg)
         if sourceImages.Count = 1
         then this.CurrentImage <- Some sourceImages.[0]
@@ -81,18 +81,17 @@ type State () =
         // Re-numbered the images.
         sourceImages |> Seq.iteri (fun i srcImg -> srcImg.num <- i + 1)
 
-    member this.SetResult (imgNum: int) (rbcRadius: float) (cells: Cell list) =
+    member this.SetResult (imgNum: int) (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 tolerance = (float sourceImage.config.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)
index f22afdb..c90fc0c 100644 (file)
@@ -18,7 +18,7 @@ type RBC = {
 
 type SourceImage = {
     mutable num: int
-    mutable rbcRadius: float
+    mutable config: Config.Config
     mutable dateLastAnalysis: DateTime // UTC.
     img: Image<Bgr, byte>
     mutable rbcs: RBC list }
\ No newline at end of file
index 7f55c7c..a7fdf71 100644 (file)
@@ -1,6 +1,7 @@
 module ImageAnalysis
 
 open System
+open System.Linq
 open System.Drawing
 
 open FSharp.Collections.ParallelSeq
@@ -20,42 +21,60 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
         | Some f -> f percent
         | _ -> ()
 
+    let inline buildLogWithName (text: string) = sprintf "(%s) %s" name text
+    let logWithName = buildLogWithName >> log
+    let inline logTimeWithName (text: string) (f: unit -> 'a) : 'a = logTime (buildLogWithName text) f
+
+    logWithName "Starting analysis ..."
+
     use green = img.Item(1)
     let greenFloat = green.Convert<Gray, float32>()
     let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation
 
-    let initialAreaOpening = int<| config.RBCArea * config.Parameters.ratioAreaPaleCenter
-    logTime "Area opening number one" (fun () -> ImgTools.areaOpenF filteredGreen initialAreaOpening)
+    logWithName (sprintf "Nominal erytrocyte diameter: %s" config.FormattedRadius)
+
+    let initialAreaOpening = int<| config.RBCArea * config.Parameters.ratioAreaPaleCenter * 1.2f // We do an area opening a little larger to avoid to do a second one in the case the radius found is near the initial one.
+    logTimeWithName "Area opening number one" (fun () -> ImgTools.areaOpenF filteredGreen initialAreaOpening)
+
     report 8
 
     let range =
         let delta = config.Parameters.granulometryRange * config.RBCRadius
         int <| config.RBCRadius - delta, int <| config.RBCRadius + delta
-    //let r1 = logTime "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) range 1.0 |> float32)
-    let r2 = logTime "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen range |> float32)
+    //let r1 = log "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) range 1.0 |> float32)
+    let r2 = logTimeWithName "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen range |> float32)
     // log (sprintf "r1: %A, r2: %A" r1 r2)
     config.RBCRadius <- r2
+
+    logWithName (sprintf "Found erytrocyte diameter: %s" config.FormattedRadius)
+
     report 24
 
     let secondAreaOpening = int <| config.RBCArea * config.Parameters.ratioAreaPaleCenter
     if secondAreaOpening > initialAreaOpening
     then
-        logTime "Area opening number two" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening)
+        logTimeWithName "Area opening number two" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening)
 
     let parasites, filteredGreenWhitoutStain = ParasitesMarker.find filteredGreen config
     //let parasites, filteredGreenWhitoutInfection, filteredGreenWhitoutStain = ParasitesMarker.findMa greenFloat filteredGreenFloat config
 
-    let edges, xGradient, yGradient = logTime "Finding edges" (fun () -> ImgTools.findEdges filteredGreenWhitoutStain)
-    logTime "Removing small connected components from thinning" (fun () -> removeArea edges (config.RBCRadius ** 2.f / 50.f |> int))
+    let edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () ->
+        let edges, xGradient, yGradient = ImgTools.findEdges filteredGreenWhitoutStain
+        removeArea edges (config.RBCRadius ** 2.f / 50.f |> int)
+        edges, xGradient, yGradient)
 
-    let allEllipses, ellipses = logTime "Finding ellipses" (fun () ->
+    let allEllipses, ellipses = logTimeWithName "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)
+    let cells = logTimeWithName "Classifier" (fun () -> Classifier.findCells ellipses parasites filteredGreenWhitoutStain config)
+
     report 100
 
+    logWithName "Analysis finished"
+
     // Output pictures if debug flag is set.
     match config.Debug with
     | DebugOn output ->
@@ -106,28 +125,24 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
 
     cells
 
-// ID * cell radius * cell list.
-let doMultipleAnalysis (imgs: (string * Image<Bgr, byte>) list) (config : Config) (reportProgress: (int -> unit) option) : (string * float * Cell list) list =
+// ID * cell list.
+let doMultipleAnalysis (imgs: (string * Config * Image<Bgr, byte>) list) (reportProgress: (int -> unit) option) : (string * Cell list) list =
     let inline report (percent: int) =
         match reportProgress with
         | Some f -> f percent
         | _ -> ()
 
-    let monitor = Object()
-    let mutable total = 0
+    let progressPerAnalysis = System.Collections.Concurrent.ConcurrentDictionary<string, int>()
     let nbImgs = List.length imgs
-    let reportProgressImg =
-        (fun progress ->
-            lock monitor (fun () -> total <- total + progress)
-            report (total / nbImgs))
+
+    let reportProgressImg (id: string) (progress: int) =
+        progressPerAnalysis.AddOrUpdate(id, progress, (fun _ _ -> progress)) |> ignore
+        report (progressPerAnalysis.Values.Sum() / nbImgs)
 
     let nbConcurrentTaskLimit = 4
     let n = Environment.ProcessorCount
 
     imgs
-    |> PSeq.map (fun (id, img) ->
-        let localConfig = config.Copy()
-        let cells = doAnalysis img id localConfig (Some reportProgressImg)
-        id, float localConfig.RBCRadius, cells)
+    |> PSeq.map (fun (id, config, img) -> id, doAnalysis img id config (Some (fun p -> reportProgressImg id p)))
     |> PSeq.withDegreeOfParallelism (if n > nbConcurrentTaskLimit then nbConcurrentTaskLimit else n)
     |> PSeq.toList
index 590c8ea..81198db 100644 (file)
@@ -95,6 +95,8 @@
     <Compile Include="GUI\NumericUpDown.xaml.fs" />
     <Resource Include="GUI\ImageSourcePreview.xaml" />
     <Compile Include="GUI\ImageSourcePreview.xaml.fs" />
+    <Resource Include="GUI\ImageSourceSelection.xaml" />
+    <Compile Include="GUI\ImageSourceSelection.xaml.fs" />
     <Resource Include="GUI\RBCFrame.xaml" />
     <Compile Include="GUI\RBCFrame.xaml.fs" />
     <Resource Include="GUI\AnalysisWindow.xaml" />
       <HintPath>..\packages\FSharp.Core.4.0.0.1\lib\net40\FSharp.Core.dll</HintPath>
       <Private>True</Private>
     </Reference>
-    <Reference Include="FSharp.Data">
-      <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="mscorlib" />
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="PresentationCore" />
     <Reference Include="PresentationFramework" />
     <Reference Include="System" />
index fcb71da..a40e463 100644 (file)
@@ -70,13 +70,15 @@ let find (filteredGreen: Image<Gray, float32>) (config: Config.Config) : Result
         diff._ThresholdBinary(Gray(0.0), Gray(255.))
         diff.Convert<Gray, byte>()
 
-    let infectionMarker = marker filteredGreen filteredGreenWithoutInfection config.Parameters.infectionLevel
-    let stainMarker = marker filteredGreenWithoutInfection filteredGreenWithoutStain config.Parameters.stainLevel
+    let infectionMarker = marker filteredGreen filteredGreenWithoutInfection (1. / config.Parameters.infectionSensitivity)
+    let stainMarker = marker filteredGreenWithoutInfection filteredGreenWithoutStain (1. / config.Parameters.stainSensitivity)
 
     // 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
index 2aa3ee4..2dcd2ff 100644 (file)
@@ -63,13 +63,13 @@ let main args =
             use resultFile = new StreamWriter(new FileStream(Path.Combine(output, "results.txt"), FileMode.Append, FileAccess.Write))
 
             //try
-            let images = [ 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), config.Copy(), new Image<Bgr, byte>(file) ]
 
 
             Utils.logTime "Whole analyze" (fun () ->
-                let results = ImageAnalysis.doMultipleAnalysis images config None
+                let results = ImageAnalysis.doMultipleAnalysis images 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 ff9ea67..6a3b745 100644 (file)
@@ -1,18 +1,16 @@
 module UnitsOfMeasure
 
-[<Measure>]
-type px
+[<Measure>] type px
+[<Measure>] type μm
+[<Measure>] type inch
+[<Measure>] type ppi = px / inch
 
-[<Measure>]
-type μm =
-    static member ConvertToInch(x: float<μm>) : float<inch> =
-        x * 1.<inch> / 25.4e3<μm>
+let μmInchRatio = 25.4e3<μm/inch>
 
-and [<Measure>] inch
+let μmToInch(x: float<μm>) : float<inch> = x / μmInchRatio
+let inchToμm(x: float<inch>) : float<μm> = x * μmInchRatio
 
 
-[<Measure>]
-type ppi = px / inch
 
 
 
index b3965a3..7f36e2b 100644 (file)
@@ -10,4 +10,5 @@
   <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" />
+  <package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
 </packages>
\ No newline at end of file