Add a DPI calculator to help to find the correct image resolution.
authorGreg Burri <greg.burri@gmail.com>
Thu, 28 Jan 2016 08:20:51 +0000 (09:20 +0100)
committerGreg Burri <greg.burri@gmail.com>
Thu, 28 Jan 2016 08:20:51 +0000 (09:20 +0100)
19 files changed:
Parasitemia/ParasitemiaCore/Analysis.fs [new file with mode: 0644]
Parasitemia/ParasitemiaCore/AssemblyInfo.fs
Parasitemia/ParasitemiaCore/Config.fs
Parasitemia/ParasitemiaCore/MainAnalysis.fs [deleted file]
Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj
Parasitemia/ParasitemiaCore/Types.fs
Parasitemia/ParasitemiaCore/UnitsOfMeasure.fs
Parasitemia/ParasitemiaUI/About.fs
Parasitemia/ParasitemiaUI/Analysis.fs
Parasitemia/ParasitemiaUI/AssemblyInfo.fs
Parasitemia/ParasitemiaUI/DPICalculator.fs [new file with mode: 0644]
Parasitemia/ParasitemiaUI/GUI.fs
Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj
Parasitemia/ParasitemiaUI/XAML/AboutWindow.xaml.fs
Parasitemia/ParasitemiaUI/XAML/AnalysisWindow.xaml.fs
Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml [new file with mode: 0644]
Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml.fs [new file with mode: 0644]
Parasitemia/ParasitemiaUI/XAML/ImageSourceSelection.xaml
Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml.fs

diff --git a/Parasitemia/ParasitemiaCore/Analysis.fs b/Parasitemia/ParasitemiaCore/Analysis.fs
new file mode 100644 (file)
index 0000000..bad0daf
--- /dev/null
@@ -0,0 +1,193 @@
+module ParasitemiaCore.Analysis
+
+open System
+open System.Linq
+open System.Drawing
+
+open FSharp.Collections.ParallelSeq
+
+open Emgu.CV
+open Emgu.CV.Structure
+
+open Logger
+
+open Utils
+open Morpho
+open ImgTools
+open Config
+open Types
+
+/// <summary>
+/// Analyze the given image and detect reb blood cell (RBC) in it.
+/// </summary>
+/// <param name="img">The image</param>
+/// <param name="name">The name, used during logging</param>
+/// <param name="config">The configuration, must not be shared with another analysis</param>
+/// <param name="reportProgress">An optional function to report progress and/or cancel the process.
+///     The first call returning 'false' will cancel the analysis.
+///     The 'int' parameter correspond to the progression from 0 to 100</param>
+/// <returns>A list of detected cells or nothing if the process has been cancelled</returns>
+let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportProgress: (int -> bool) option) : Cell list option =
+
+    // To report the progress of this function from 0 to 100.
+    // Return 'None' if the process must be aborted.
+    let reportWithVal (percent: int) (value: 'a) : 'a option =
+        match reportProgress with
+        | Some f ->
+            if f percent
+            then Some value
+            else None
+        | _ -> Some value
+
+    let report (percent: int) : unit option =
+        reportWithVal percent ()
+
+    let inline buildLogWithName (text: string) = sprintf "(%s) %s" name text
+    let logWithName mess = Log.User(buildLogWithName mess)
+    let inline logTimeWithName (text: string) (f: unit -> 'a option) : 'a option = Log.LogWithTime((buildLogWithName text), Severity.USER, f)
+
+    maybe {
+        do! report 0
+
+        logWithName "Starting analysis ..."
+
+        use img_float = img.Convert<Bgr, float32>()
+
+        use img_RBC = img_float.[1] // mergeChannelsWithProjection img_float config.Parameters.averageColor_RBC config.Parameters.averageColor_BG 255.
+        use img_RBC_filtered = gaussianFilter img_RBC config.LPFStandardDeviationRBC
+
+        use img_parasites = img_float.[2] // mergeChannelsWithProjection img_float config.Parameters.averageColor_Parasite config.Parameters.averageColor_RBC 255.
+        use img_parasites_filtered = gaussianFilter img_parasites config.LPFStandardDeviationParasite
+
+        logWithName (sprintf "Nominal erythrocyte diameter: %A" config.RBCRadiusByResolution)
+
+        let initialAreaOpening = int <| config.RBCRadiusByResolution.Area * config.Parameters.ratioAreaPaleCenter * 1.1f // 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.
+        do! logTimeWithName "First area opening" (fun () -> areaOpenF img_RBC_filtered initialAreaOpening; report 10)
+
+        let range =
+            let delta = config.Parameters.granulometryRange * config.RBCRadiusByResolution.Pixel
+            int <| config.RBCRadiusByResolution.Pixel - delta, int <| config.RBCRadiusByResolution.Pixel + delta
+        let! radius = logTimeWithName "Granulometry (area)" (fun() -> reportWithVal 10 (Granulometry.findRadiusByAreaClosing img_RBC_filtered range |> float32))
+        config.SetRBCRadius <| radius
+
+        logWithName (sprintf "Found erythrocyte diameter: %A" config.RBCRadius)
+
+        do! report 20
+
+        do!
+            let secondAreaOpening = int <| config.RBCRadius.Area * config.Parameters.ratioAreaPaleCenter
+            if secondAreaOpening > initialAreaOpening
+            then
+                logTimeWithName "Second area opening" (fun () -> areaOpenF img_RBC_filtered secondAreaOpening; report 30)
+            else
+                report 30
+
+        // Remove pale centers from the parasites image.
+        areaOpenF img_parasites_filtered (int <| config.RBCRadiusByResolution.Area * config.Parameters.ratioAreaPaleCenter)
+
+        // Removing parasites.
+        areaCloseF img_RBC_filtered (roundInt <| Const.PI * config.RBCRadius.ParasiteRadius ** 2.f)
+
+        let! parasites, imgWhitoutParasite, imgWithoutNucleus =
+            logTimeWithName "Parasites segmentation" (fun () -> reportWithVal 40 (ParasitesMarker.find img_parasites_filtered config))
+
+        let! edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () ->
+            let edges, xGradient, yGradient = Edges.find img_RBC_filtered
+            removeArea edges (config.RBCRadius.Pixel ** 2.f / 50.f |> int)
+            reportWithVal 50 (edges, xGradient, yGradient))
+
+        let! matchingEllipses = logTimeWithName "Finding ellipses" (fun () -> reportWithVal 60 (Ellipse.find edges xGradient yGradient config))
+
+        let! prunedEllipses = logTimeWithName "Ellipses pruning" (fun () -> reportWithVal 80 (matchingEllipses.PrunedEllipses))
+
+        let! cells = logTimeWithName "Classifier" (fun () -> reportWithVal 100 (Classifier.findCells prunedEllipses parasites img_RBC_filtered config))
+
+        logWithName "Analysis finished"
+
+        do
+            // Output pictures if debug flag is set.
+            match config.Debug with
+            | DebugOn output ->
+                let dirPath = System.IO.Path.Combine(output, name)
+                System.IO.Directory.CreateDirectory dirPath |> ignore
+
+                let buildFileName postfix = System.IO.Path.Combine(dirPath, name + postfix)
+
+                IO.saveMat (edges * 255.0) (buildFileName " - edges.png")
+
+                IO.saveImg parasites.darkStain (buildFileName " - parasites - dark stain.png")
+                IO.saveImg parasites.parasite (buildFileName " - parasites - stain.png")
+                IO.saveImg parasites.nucleus (buildFileName " - parasites - infection.png")
+
+                let imgAllEllipses = img.Copy()
+                Drawing.drawEllipses imgAllEllipses matchingEllipses.Ellipses (Bgr(255.0, 255.0, 255.0)) 0.04
+                IO.saveImg imgAllEllipses (buildFileName " - ellipses - all.png")
+
+                let imgEllipses = img_RBC_filtered.Convert<Bgr, byte>()
+                Drawing.drawEllipses imgEllipses prunedEllipses (Bgr(0.0, 240.0, 240.0)) 1.0
+                IO.saveImg imgEllipses (buildFileName " - ellipses.png")
+
+                let imgCells = img.Copy()
+                Drawing.drawCells imgCells false cells
+                IO.saveImg imgCells (buildFileName " - cells.png")
+
+                let imgCells' = img.Copy()
+                Drawing.drawCells imgCells' true cells
+                IO.saveImg imgCells' (buildFileName " - cells - full.png")
+
+                let filteredGreenMaxima = gaussianFilter img_RBC config.LPFStandardDeviationRBC
+                for m in findMaxima filteredGreenMaxima do
+                    Drawing.drawPoints filteredGreenMaxima m 255.f
+                IO.saveImg filteredGreenMaxima (buildFileName " - filtered - maxima.png")
+
+                IO.saveImg imgWhitoutParasite (buildFileName " - filtered closed stain.png")
+                IO.saveImg imgWithoutNucleus (buildFileName " - filtered closed infection.png")
+
+                IO.saveImg img_RBC_filtered (buildFileName " - source - RBC.png")
+                IO.saveImg img_parasites_filtered (buildFileName " - source - parasites.png")
+
+                IO.saveImg (normalize img_float.[2] 255.) (buildFileName " - source - red.png")
+                IO.saveImg (normalize img_float.[1] 255.) (buildFileName " - source - green.png")
+                IO.saveImg (normalize img_float.[0] 255.) (buildFileName " - source - blue.png")
+            | _ -> ()
+
+        return cells }
+
+/// <summary>
+/// Do multiple analyses on the same time. The number of concurrent process depends if the number of the core.
+/// </summary>
+/// <param name="imgs">The images: (name * configuration * image)</param>
+/// <param name="reportProgress">An optional function to report progress and/or cancel the process.
+///     The first call returning 'false' will cancel the analysis.
+///     The 'int' parameter correspond to the progression from 0 to 100</param>
+/// <returns>'None' if the process has been cancelled or the list of result as (name * cells), 'name' corresponds to the given name<returns>
+let doMultipleAnalysis (imgs: (string * Config * Image<Bgr, byte>) list) (reportProgress: (int -> bool) option) : (string * Cell list) list option =
+    let report (percent: int) : bool =
+        match reportProgress with
+        | Some f -> f percent
+        | _ -> true
+
+    let progressPerAnalysis = System.Collections.Concurrent.ConcurrentDictionary<string, int>()
+    let nbImgs = List.length imgs
+
+    let reportProgressImg (id: string) (progress: int) =
+        progressPerAnalysis.AddOrUpdate(id, progress, (fun _ _ -> progress)) |> ignore
+        report (progressPerAnalysis.Values.Sum() / nbImgs)
+
+    let n = Environment.ProcessorCount
+
+    let results =
+        imgs
+        |> PSeq.choose (
+            fun (id, config, img) ->
+                match doAnalysis img id config (Some (fun p -> reportProgressImg id p)) with
+                | Some result -> Some (id, result)
+                | None -> None)
+        |> PSeq.withDegreeOfParallelism n
+        |> PSeq.toList
+
+    // If one of the analyses has been aborted we return 'None'.
+    if List.length results <> List.length imgs
+    then None
+    else Some results
+
index c129f42..21b305e 100644 (file)
@@ -34,8 +34,8 @@ open System.Runtime.InteropServices
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [<assembly: AssemblyVersion("1.0.*")>]
-[<assembly: AssemblyVersion("1.0.0.4")>]
-[<assembly: AssemblyFileVersion("1.0.0.4")>]
+[<assembly: AssemblyVersion("1.0.0.5")>]
+[<assembly: AssemblyFileVersion("1.0.0.5")>]
 
 do
     ()
\ No newline at end of file
index 9724d9c..385b7d4 100644 (file)
@@ -42,7 +42,7 @@ type Parameters = {
     minimumCellAreaFactor: float32 } // Factor of the mean RBC area. A cell with an area below this will be rejected.
 
 let defaultParameters = {
-    rbcDiameter = 8.<μm>
+    rbcDiameter = 7.5<μm>
     resolution = 220.e3<ppi> // 220.e3<ppi> Correspond to 50X.
 
     ratioAreaPaleCenter = 2.f / 5.f // The ratio between an RBC area and the area of the its pale center.
diff --git a/Parasitemia/ParasitemiaCore/MainAnalysis.fs b/Parasitemia/ParasitemiaCore/MainAnalysis.fs
deleted file mode 100644 (file)
index 926aeac..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-module ParasitemiaCore.Analysis
-
-open System
-open System.Linq
-open System.Drawing
-
-open FSharp.Collections.ParallelSeq
-
-open Emgu.CV
-open Emgu.CV.Structure
-
-open Logger
-
-open Utils
-open Morpho
-open ImgTools
-open Config
-open Types
-
-/// <summary>
-/// Analyze the given image and detect reb blood cell (RBC) in it.
-/// </summary>
-/// <param name="img">The image</param>
-/// <param name="name">The name, used during logging</param>
-/// <param name="config">The configuration, must not be shared with another analysis</param>
-/// <param name="reportProgress">An optional function to report progress and/or cancel the process.
-///     The first call returning 'false' will cancel the analysis.
-///     The 'int' parameter correspond to the progression from 0 to 100</param>
-/// <returns>A list of detected cells or nothing if the process has been cancelled</returns>
-let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportProgress: (int -> bool) option) : Cell list option =
-
-    // To report the progress of this function from 0 to 100.
-    // Return 'None' if the process must be aborted.
-    let reportWithVal (percent: int) (value: 'a) : 'a option =
-        match reportProgress with
-        | Some f ->
-            if f percent
-            then Some value
-            else None
-        | _ -> Some value
-
-    let report (percent: int) : unit option =
-        reportWithVal percent ()
-
-    let inline buildLogWithName (text: string) = sprintf "(%s) %s" name text
-    let logWithName mess = Log.User(buildLogWithName mess)
-    let inline logTimeWithName (text: string) (f: unit -> 'a option) : 'a option = Log.LogWithTime((buildLogWithName text), Severity.USER, f)
-
-    maybe {
-        do! report 0
-
-        logWithName "Starting analysis ..."
-
-        use img_float = img.Convert<Bgr, float32>()
-
-        use img_RBC = img_float.[1] // mergeChannelsWithProjection img_float config.Parameters.averageColor_RBC config.Parameters.averageColor_BG 255.
-        use img_RBC_filtered = gaussianFilter img_RBC config.LPFStandardDeviationRBC
-
-        use img_parasites = img_float.[2] // mergeChannelsWithProjection img_float config.Parameters.averageColor_Parasite config.Parameters.averageColor_RBC 255.
-        use img_parasites_filtered = gaussianFilter img_parasites config.LPFStandardDeviationParasite
-
-        logWithName (sprintf "Nominal erythrocyte diameter: %A" config.RBCRadiusByResolution)
-
-        let initialAreaOpening = int <| config.RBCRadiusByResolution.Area * config.Parameters.ratioAreaPaleCenter * 1.1f // 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.
-        do! logTimeWithName "First area opening" (fun () -> areaOpenF img_RBC_filtered initialAreaOpening; report 10)
-
-        let range =
-            let delta = config.Parameters.granulometryRange * config.RBCRadiusByResolution.Pixel
-            int <| config.RBCRadiusByResolution.Pixel - delta, int <| config.RBCRadiusByResolution.Pixel + delta
-        let! radius = logTimeWithName "Granulometry (area)" (fun() -> reportWithVal 10 (Granulometry.findRadiusByAreaClosing img_RBC_filtered range |> float32))
-        config.SetRBCRadius <| radius
-
-        logWithName (sprintf "Found erythrocyte diameter: %A" config.RBCRadius)
-
-        do! report 20
-
-        do!
-            let secondAreaOpening = int <| config.RBCRadius.Area * config.Parameters.ratioAreaPaleCenter
-            if secondAreaOpening > initialAreaOpening
-            then
-                logTimeWithName "Second area opening" (fun () -> areaOpenF img_RBC_filtered secondAreaOpening; report 30)
-            else
-                report 30
-
-        // Removing parasites.
-        areaCloseF img_RBC_filtered (roundInt <| Const.PI * config.RBCRadius.ParasiteRadius ** 2.f)
-
-        let! parasites, imgWhitoutParasite, imgWithoutNucleus =
-            logTimeWithName "Parasites segmentation" (fun () -> reportWithVal 40 (ParasitesMarker.find img_parasites_filtered config))
-
-        let! edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () ->
-            let edges, xGradient, yGradient = Edges.find img_RBC_filtered
-            removeArea edges (config.RBCRadius.Pixel ** 2.f / 50.f |> int)
-            reportWithVal 50 (edges, xGradient, yGradient))
-
-        let! matchingEllipses = logTimeWithName "Finding ellipses" (fun () -> reportWithVal 60 (Ellipse.find edges xGradient yGradient config))
-
-        let! prunedEllipses = logTimeWithName "Ellipses pruning" (fun () -> reportWithVal 80 (matchingEllipses.PrunedEllipses))
-
-        let! cells = logTimeWithName "Classifier" (fun () -> reportWithVal 100 (Classifier.findCells prunedEllipses parasites img_RBC_filtered config))
-
-        logWithName "Analysis finished"
-
-        do
-            // Output pictures if debug flag is set.
-            match config.Debug with
-            | DebugOn output ->
-                let dirPath = System.IO.Path.Combine(output, name)
-                System.IO.Directory.CreateDirectory dirPath |> ignore
-
-                let buildFileName postfix = System.IO.Path.Combine(dirPath, name + postfix)
-
-                IO.saveMat (edges * 255.0) (buildFileName " - edges.png")
-
-                IO.saveImg parasites.darkStain (buildFileName " - parasites - dark stain.png")
-                IO.saveImg parasites.parasite (buildFileName " - parasites - stain.png")
-                IO.saveImg parasites.nucleus (buildFileName " - parasites - infection.png")
-
-                let imgAllEllipses = img.Copy()
-                Drawing.drawEllipses imgAllEllipses matchingEllipses.Ellipses (Bgr(255.0, 255.0, 255.0)) 0.04
-                IO.saveImg imgAllEllipses (buildFileName " - ellipses - all.png")
-
-                let imgEllipses = img_RBC_filtered.Convert<Bgr, byte>()
-                Drawing.drawEllipses imgEllipses prunedEllipses (Bgr(0.0, 240.0, 240.0)) 1.0
-                IO.saveImg imgEllipses (buildFileName " - ellipses.png")
-
-                let imgCells = img.Copy()
-                Drawing.drawCells imgCells false cells
-                IO.saveImg imgCells (buildFileName " - cells.png")
-
-                let imgCells' = img.Copy()
-                Drawing.drawCells imgCells' true cells
-                IO.saveImg imgCells' (buildFileName " - cells - full.png")
-
-                let filteredGreenMaxima = gaussianFilter img_RBC config.LPFStandardDeviationRBC
-                for m in findMaxima filteredGreenMaxima do
-                    Drawing.drawPoints filteredGreenMaxima m 255.f
-                IO.saveImg filteredGreenMaxima (buildFileName " - filtered - maxima.png")
-
-                IO.saveImg img_RBC_filtered (buildFileName " - filtered.png")
-                IO.saveImg imgWhitoutParasite (buildFileName " - filtered closed stain.png")
-                IO.saveImg imgWithoutNucleus (buildFileName " - filtered closed infection.png")
-
-                IO.saveImg img_RBC (buildFileName " - source - RBC.png")
-                IO.saveImg img_parasites (buildFileName " - source - parasites.png")
-
-                IO.saveImg (normalize img_float.[2] 255.) (buildFileName " - source - red.png")
-                IO.saveImg (normalize img_float.[1] 255.) (buildFileName " - source - green.png")
-                IO.saveImg (normalize img_float.[0] 255.) (buildFileName " - source - blue.png")
-            | _ -> ()
-
-        return cells }
-
-/// <summary>
-/// Do multiple analyses on the same time. The number of concurrent process depends if the number of the core.
-/// </summary>
-/// <param name="imgs">The images: (name * configuration * image)</param>
-/// <param name="reportProgress">An optional function to report progress and/or cancel the process.
-///     The first call returning 'false' will cancel the analysis.
-///     The 'int' parameter correspond to the progression from 0 to 100</param>
-/// <returns>'None' if the process has been cancelled or the list of result as (name * cells), 'name' corresponds to the given name<returns>
-let doMultipleAnalysis (imgs: (string * Config * Image<Bgr, byte>) list) (reportProgress: (int -> bool) option) : (string * Cell list) list option =
-    let report (percent: int) : bool =
-        match reportProgress with
-        | Some f -> f percent
-        | _ -> true
-
-    let progressPerAnalysis = System.Collections.Concurrent.ConcurrentDictionary<string, int>()
-    let nbImgs = List.length imgs
-
-    let reportProgressImg (id: string) (progress: int) =
-        progressPerAnalysis.AddOrUpdate(id, progress, (fun _ _ -> progress)) |> ignore
-        report (progressPerAnalysis.Values.Sum() / nbImgs)
-
-    let n = Environment.ProcessorCount
-
-    let results =
-        imgs
-        |> PSeq.choose (
-            fun (id, config, img) ->
-                match doAnalysis img id config (Some (fun p -> reportProgressImg id p)) with
-                | Some result -> Some (id, result)
-                | None -> None)
-        |> PSeq.withDegreeOfParallelism n
-        |> PSeq.toList
-
-    // If one of the analyses has been aborted we return 'None'.
-    if List.length results <> List.length imgs
-    then None
-    else Some results
-
index 6a9eb75..473f11d 100644 (file)
@@ -67,7 +67,7 @@
     <Compile Include="MatchingEllipses.fs" />
     <Compile Include="Ellipse.fs" />
     <Compile Include="Classifier.fs" />
-    <Compile Include="MainAnalysis.fs" />
+    <Compile Include="Analysis.fs" />
     <Content Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
index 8f6c34a..5440115 100644 (file)
@@ -86,4 +86,18 @@ type MaybeBuilder () =
     member this.Return (x) =
         Some x
 
-let maybe = MaybeBuilder()
\ No newline at end of file
+let maybe = MaybeBuilder()
+
+type Result<'a> =
+    | Success of 'a
+    | Fail of string // Error message.
+
+type ResultBuilder () =
+    member this.Bind (res, f) =
+        match res with
+        | Success value -> f value
+        | fail -> fail
+
+    member this.ReturnFrom (x) = x
+
+let result = ResultBuilder()
\ No newline at end of file
index 36c9495..637f49e 100644 (file)
@@ -1,12 +1,17 @@
 module ParasitemiaCore.UnitsOfMeasure
 
 [<Measure>] type px // Pixel.
+[<Measure>] type mm
 [<Measure>] type μm
 [<Measure>] type inch
 [<Measure>] type ppi = px / inch // Pixel per inch.
 
 let μmInchRatio = 25.4e3<μm/inch>
+let mmInchRatio = 25.4<mm/inch>
 
 let μmToInch(x: float<μm>) : float<inch> = x / μmInchRatio
 let inchToμm(x: float<inch>) : float<μm> = x * μmInchRatio
 
+let mmToInch(x: float<mm>) : float<inch> = x / mmInchRatio
+let inchTomm(x: float<inch>) : float<mm> = x * mmInchRatio
+
index 44dd881..18f266f 100644 (file)
@@ -9,37 +9,28 @@ open System.Windows.Controls
 open System.Diagnostics
 
 let showWindow (parent: Window) =
-    let window = Views.AboutWindow()
-    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 txtAbout: TextBlock = ctrl "txtAbout"
-
-    let linkHESSO: Documents.Hyperlink = ctrl "linkHESSO"
-    let linkCHUV: Documents.Hyperlink = ctrl "linkCHUV"
-    let linkGBurri: Documents.Hyperlink = ctrl "linkGBurri"
+    let win = Views.AboutWindow()
+    win.Root.Owner <- parent
+    win.Root.Left <- parent.Left + parent.ActualWidth / 2. - win.Root.Width / 2.
+    win.Root.Top <- parent.Top + parent.ActualHeight / 2. - win.Root.Height / 2.
 
     let version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version
     let txtVersion = sprintf " %d.%d.%d" version.Major version.Minor version.Revision
-    txtAbout.Inlines.FirstInline.ElementEnd.InsertTextInRun(txtVersion)
+    win.txtAbout.Inlines.FirstInline.ElementEnd.InsertTextInRun(txtVersion)
 
     let navigateTo = Navigation.RequestNavigateEventHandler(fun obj args ->
         Process.Start(ProcessStartInfo(args.Uri.AbsoluteUri)) |> ignore
         args.Handled <- true)
 
-    linkHESSO.RequestNavigate.AddHandler(navigateTo);
-    linkCHUV.RequestNavigate.AddHandler(navigateTo);
-    linkGBurri.RequestNavigate.AddHandler(navigateTo);
+    win.linkHESSO.RequestNavigate.AddHandler(navigateTo);
+    win.linkCHUV.RequestNavigate.AddHandler(navigateTo);
+    win.linkGBurri.RequestNavigate.AddHandler(navigateTo);
 
 #if DEBUG
-    txtAbout.Inlines.FirstInline.ElementEnd.InsertTextInRun(" - DEBUG")
+    win.txtAbout.Inlines.FirstInline.ElementEnd.InsertTextInRun(" - DEBUG")
 #endif
 
-    butClose.Click.AddHandler(fun obj args -> window.Root.Close())
+    win.butClose.Click.AddHandler(fun obj args -> win.Root.Close())
 
-    window.Root.ShowDialog() |> ignore
+    win.Root.ShowDialog() |> ignore
 
index 9b9ed51..70d9ef0 100644 (file)
@@ -19,38 +19,27 @@ open ParasitemiaCore.Config
 open Types
 
 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.
-    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 stackSourceImagesSelection: StackPanel = ctrl "stackSourceImagesSelection"
-    let progressBar: ProgressBar = ctrl "progress"
-    let textLog: TextBlock = ctrl "textLog"
-    let scrollLog: ScrollViewer = ctrl "scrollLog"
+    let win = Views.AnalysisWindow()
+    win.Root.Owner <- parent
+    win.Root.Left <- parent.Left + parent.ActualWidth / 2. - win.Root.Width / 2.
+    win.Root.Top <- parent.Top + parent.ActualHeight / 2. - win.Root.Height / 2.
 
     let logListener =
         { new Logger.IListener with
             member this.NewEntry severity mess =
-                window.Root.Dispatcher.Invoke(fun () ->
-                textLog.Inlines.Add(Documents.Run(mess))
-                textLog.Inlines.Add(Documents.LineBreak())
-                scrollLog.ScrollToBottom()) }
+                win.Root.Dispatcher.Invoke(fun () ->
+                    win.textLog.Inlines.Add(Documents.Run(mess))
+                    win.textLog.Inlines.Add(Documents.LineBreak())
+                    win.scrollLog.ScrollToBottom()) }
 
     Logger.Log.AddListener(logListener)
 
     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
+        match Double.TryParse(input) with
+        | true, value when value >= minPPI && value <= maxPPI -> Some value
+        | _ -> None
 
     let monitor = Object()
     let mutable atLeastOneAnalysisPerformed = false
@@ -58,8 +47,8 @@ let showWindow (parent: Window) (state: State.State) : bool =
     let mutable analysisCancelled = false
 
     let updateSourceImages () =
-        stackSourceImagesSelection.Children.Clear()
-        let width = int stackSourceImagesSelection.ActualWidth
+        win.stackSourceImagesSelection.Children.Clear()
+        let width = int win.stackSourceImagesSelection.ActualWidth
         for srcImg in state.SourceImages do
             let imageSourceSelection = Views.ImageSourceSelection(Tag = srcImg, Margin = Thickness(3.))
             imageSourceSelection.Tag <- srcImg
@@ -74,6 +63,11 @@ let showWindow (parent: Window) (state: State.State) : bool =
             imageSourceSelection.menuZoom50X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "230000")
             imageSourceSelection.menuZoom100X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "460000")
 
+            imageSourceSelection.butDPICalculator.Click.AddHandler(fun obj args ->
+                match DPICalculator.showWindow win.Root with
+                | Some resolution -> imageSourceSelection.txtResolution.Text <- resolution.ToString()
+                | None -> ())
+
             imageSourceSelection.txtResolution.PreviewTextInput.AddHandler(fun obj args ->
                 let text = imageSourceSelection.txtResolution.Text + args.Text
                 args.Handled <- match parseAndValidatePPI text with Some _ -> false | None -> true)
@@ -82,12 +76,12 @@ let showWindow (parent: Window) (state: State.State) : bool =
                 let checkbox = imageSourceSelection.chkSelection
                 checkbox.IsChecked <- Nullable<bool>(not (checkbox.IsChecked.HasValue && checkbox.IsChecked.Value)))
 
-            stackSourceImagesSelection.Children.Add(imageSourceSelection) |> ignore
+            win.stackSourceImagesSelection.Children.Add(imageSourceSelection) |> ignore
 
     // Get the new parameters for each image. If an error occurs then 'None' is returned and a message box is displayed.
     // The boolean is 'true' if the image is selected (checked).
     let getInputImagesParameters () : (SourceImage * bool * Parameters) list option =
-        let sourceImagesControls = stackSourceImagesSelection.Children |> Seq.cast<Views.ImageSourceSelection>
+        let sourceImagesControls = win.stackSourceImagesSelection.Children |> Seq.cast<Views.ImageSourceSelection>
         let parameters = seq {
             for srcImgCtrl in sourceImagesControls do
                 let srcImg = srcImgCtrl.Tag :?> SourceImage
@@ -102,9 +96,9 @@ let showWindow (parent: Window) (state: State.State) : bool =
         then None
         else Some parameters
 
-    butClose.Click.AddHandler(fun obj args -> window.Root.Close())
+    win.butClose.Click.AddHandler(fun obj args -> win.Root.Close())
 
-    butStart.Click.AddHandler(fun obj args ->
+    win.butStart.Click.AddHandler(fun obj args ->
         match getInputImagesParameters () with
         | Some imagesParameters ->
             let imagesToProcess = [
@@ -117,16 +111,16 @@ let showWindow (parent: Window) (state: State.State) : bool =
             then
                 MessageBox.Show("No image selected", "Cannot start analysis", MessageBoxButton.OK, MessageBoxImage.Information) |> ignore
             else
-                stackSourceImagesSelection.IsEnabled <- false
+                win.stackSourceImagesSelection.IsEnabled <- false
                 analysisPerformed <- false
-                butStart.IsEnabled <- false
-                butClose.Content <- "Abort"
+                win.butStart.IsEnabled <- false
+                win.butClose.Content <- "Abort"
 
                 async {
                     let maybeResults =
                         ParasitemiaCore.Analysis.doMultipleAnalysis
                             imagesToProcess
-                            (Some (fun progress -> window.Root.Dispatcher.Invoke(fun () -> progressBar.Value <- float progress); not analysisCancelled))
+                            (Some (fun progress -> win.Root.Dispatcher.Invoke(fun () -> win.progress.Value <- float progress); not analysisCancelled))
 
                     lock monitor (
                         fun() ->
@@ -135,10 +129,10 @@ let showWindow (parent: Window) (state: State.State) : bool =
                                 for id, cells in results do
                                     state.SetResult (int id) cells
 
-                                window.Root.Dispatcher.Invoke(fun () ->
-                                    stackSourceImagesSelection.IsEnabled <- true
-                                    butStart.IsEnabled <- true
-                                    butClose.Content <- "Close"
+                                win.Root.Dispatcher.Invoke(fun () ->
+                                    win.stackSourceImagesSelection.IsEnabled <- true
+                                    win.butStart.IsEnabled <- true
+                                    win.butClose.Content <- "Close"
                                     updateSourceImages ())
 
                                 Logger.Log.User("All analyses terminated successfully")
@@ -148,9 +142,9 @@ let showWindow (parent: Window) (state: State.State) : bool =
                 } |> Async.Start
         | _ -> ())
 
-    window.Root.Loaded.AddHandler(fun obj args -> updateSourceImages ())
+    win.Root.Loaded.AddHandler(fun obj args -> updateSourceImages ())
 
-    window.Root.ShowDialog() |> ignore
+    win.Root.ShowDialog() |> ignore
 
     Logger.Log.RmListener(logListener)
 
index b6efc67..46c0c22 100644 (file)
@@ -34,8 +34,8 @@ open System.Runtime.InteropServices
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [<assembly: AssemblyVersion("1.0.*")>]
-[<assembly: AssemblyVersion("1.0.0.4")>]
-[<assembly: AssemblyFileVersion("1.0.0.4")>]
+[<assembly: AssemblyVersion("1.0.0.5")>]
+[<assembly: AssemblyFileVersion("1.0.0.5")>]
 
 do
     ()
\ No newline at end of file
diff --git a/Parasitemia/ParasitemiaUI/DPICalculator.fs b/Parasitemia/ParasitemiaUI/DPICalculator.fs
new file mode 100644 (file)
index 0000000..dbd7251
--- /dev/null
@@ -0,0 +1,75 @@
+module ParasitemiaUI.DPICalculator
+
+open System
+open System.Windows
+open System.Windows.Media
+open System.Windows.Markup
+open System.Windows.Shapes
+open System.Windows.Controls
+open System.Diagnostics
+
+open ParasitemiaCore.UnitsOfMeasure
+open ParasitemiaCore.Types
+
+type SensorSize = {
+    w: float<mm>
+    h: float<mm>
+    txt: string } with
+    override this.ToString () =
+        sprintf "%g mm × %g mm%s" this.w this.h (if this.txt = "" then "" else " (" + this.txt + ")")
+
+let showWindow (parent: Window) : int option =
+    let win = Views.DPICalculatorWindow()
+    win.Root.Owner <- parent
+    win.Root.Left <- parent.Left + parent.ActualWidth / 2. - win.Root.Width / 2.
+    win.Root.Top <- parent.Top + parent.ActualHeight / 2. - win.Root.Height / 2.
+
+    let sensorSizes = [
+        { w = 5.76<mm>; h = 4.29<mm>; txt = "1/2.5″" }
+        { w = 7.6<mm>;  h = 5.7<mm>;  txt = "1/1.7″" }
+        { w = 8.6<mm>;  h = 6.6<mm>;  txt = "2/3″" }
+        { w = 13.2<mm>; h = 8.8<mm>;  txt = "1″" }
+        { w = 17.3<mm>; h = 13.<mm>;  txt = "" }
+        { w = 20.7<mm>; h = 13.8<mm>; txt = "" }
+        { w = 22.2<mm>; h = 14.8<mm>; txt = "" }
+        { w = 23.6<mm>; h = 15.7<mm>; txt = "" }
+        { w = 28.7<mm>; h = 19.<mm>;  txt = "" }
+        { w = 28.7<mm>; h = 19.<mm>;  txt = "" } ]
+
+    for size in sensorSizes do
+        win.cmbSensorSize.Items.Add(size) |> ignore
+    win.cmbSensorSize.SelectedIndex <- 0
+
+    let resolution (w_p: float<px>) (w_mm: float<mm>) (zoom: float) : float<ppi> =
+        w_p * zoom / mmToInch w_mm
+
+    let updateCurrentResolution () =
+        let { w = w; h = h } = win.cmbSensorSize.SelectedValue :?> SensorSize
+        let ratio = h / w
+
+        let parseDouble txt errorMess = match Double.TryParse(txt) with true, value -> Success value | _ -> Fail errorMess
+
+        match result
+            { let! sensorResolution = parseDouble win.txtSensorResolution.Text "The sensor resolution is not valid"
+              let! zoom = parseDouble win.txtZoom.Text "The zoom is not valid"
+              let wPixel = 1.<px> * sqrt (sensorResolution * 1e6 / ratio)
+              return! Success (float <| resolution wPixel w zoom) } with
+        | Success res -> win.txtImageResolution.Text <- (int (res / 1000.) * 1000).ToString()
+        | Fail mess -> win.txtImageResolution.Text <- mess
+
+    win.butCancel.Click.AddHandler(fun obj args -> win.Root.DialogResult <- Nullable<bool>(false); win.Root.Close())
+    win.butOK.Click.AddHandler(fun obj args -> win.Root.DialogResult <- Nullable<bool>(true); win.Root.Close())
+
+    win.cmbSensorSize.SelectionChanged.AddHandler(fun obj arg -> updateCurrentResolution ())
+    win.txtSensorResolution.TextChanged.AddHandler(fun obj arg -> updateCurrentResolution ())
+    win.txtZoom.TextChanged.AddHandler(fun obj arg -> updateCurrentResolution ())
+
+    let result = win.Root.ShowDialog()
+    if result.HasValue && result.Value
+    then
+        match Int32.TryParse win.txtImageResolution.Text with
+        | true, res -> Some res
+        | _ -> None
+    else
+        None
+
index 3746461..8438b4b 100644 (file)
@@ -21,52 +21,16 @@ open Types
 
 let run (defaultConfig: Config) (fileToOpen: string option) =
     let app = new Application()
-    let mainWindow = Views.MainWindow()
-    let ctrl (name: string): 'a = mainWindow.Root.FindName(name) :?> 'a
+    let win = Views.MainWindow()
 
     let state = State.State(defaultConfig)
     let mutable currentScale = 1.
     let mutable displayHealthy = false
     let warningBelowNumberOfRBC = 1000
 
-    let menuExit: MenuItem = ctrl "menuExit"
-    let menuSaveFile: MenuItem = ctrl "menuSave"
-    let menuSaveAsFile: MenuItem = ctrl "menuSaveAs"
-    let menuLoadFile: MenuItem = ctrl "menuOpen"
-    let menuNewFile: MenuItem = ctrl "menuNew"
-    let menuExportResults: MenuItem = ctrl "menuExportResults"
-    let menuAddSourceImage: MenuItem = ctrl "menuAddSourceImage"
-    let menuAnalysis: MenuItem = ctrl "menuAnalysis"
-    let menuStartAnalysis: MenuItem = ctrl "menuStartAnalysis"
-    let menuView: MenuItem = ctrl "menuView"
-    let menuHightlightRBC: MenuItem = ctrl "menuHightlightRBC"
-    let menuAbout: MenuItem = ctrl "menuAbout"
-
-    let txtDocumentStatus: TextBlock = ctrl "txtDocumentStatus"
-    let txtMessageStatus: TextBlock = ctrl "txtMessageStatus"
-
-    let txtPatient: TextBox = ctrl "txtPatient"
-    let txtGlobalParasitemia: TextBlock = ctrl "txtGlobalParasitemia"
-
-    let stackPreviews: StackPanel = ctrl "stackPreviews"
-
-    let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage"
-    let borderCurrentImage: Border = ctrl "borderCurrentImage"
-    let canvasCurrentImage: Canvas = ctrl "canvasCurrentImage"
-
-    let gridImageInformation: Grid = ctrl "gridImageInformation"
-    let txtImageInformation1: TextBlock = ctrl "txtImageInformation1"
-    let txtImageInformation2: TextBlock = ctrl "txtImageInformation2"
-    let txtImageName: TextBox = ctrl "txtImageName"
-
-    let scrollRBC: ScrollViewer = ctrl "scrollRBC"
-    let stackRBC: StackPanel = ctrl "stackRBC"
-
-    let imgLogos: Border = ctrl "imgLogos"
-
     // Initializations.
-    let canvasCurrentImageColor = canvasCurrentImage.Background
-    menuHightlightRBC.IsChecked <- displayHealthy
+    let canvasCurrentImageColor = win.canvasCurrentImage.Background
+    win.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> =
@@ -98,13 +62,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         frame
 
     let updateDocumentStatus () =
-        txtDocumentStatus.Text <- if state.FilePath = "" then "<New document>" else state.FilePath
+        win.txtDocumentStatus.Text <- if state.FilePath = "" then "<New document>" else state.FilePath
 
     let statusMessageTimer = Threading.DispatcherTimer()
-    statusMessageTimer.Tick.AddHandler(fun obj args -> statusMessageTimer.Stop(); txtMessageStatus.Text <- "")
+    statusMessageTimer.Tick.AddHandler(fun obj args -> statusMessageTimer.Stop(); win.txtMessageStatus.Text <- "")
     statusMessageTimer.Interval <- TimeSpan(0, 0, 2)
     let displayStatusMessage (message: string) =
-        txtMessageStatus.Text <- message
+        win.txtMessageStatus.Text <- message
         statusMessageTimer.Stop()
         statusMessageTimer.Start()
 
@@ -119,71 +83,71 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             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)
-        scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
+        win.scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - win.scrollViewCurrentImage.ViewportWidth / 2. + win.borderCurrentImage.BorderThickness.Left)
+        win.scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - win.scrollViewCurrentImage.ViewportHeight / 2. + win.borderCurrentImage.BorderThickness.Top)
 
 
     let txtImageName_TextChanged =
-        TextChangedEventHandler(fun obj args -> state.CurrentImage |> Option.iter(fun srcImg -> state.SetName srcImg txtImageName.Text))
+        TextChangedEventHandler(fun obj args -> state.CurrentImage |> Option.iter(fun srcImg -> state.SetName srcImg win.txtImageName.Text))
 
     let updateCurrentImageInformation () =
-        txtImageName.TextChanged.RemoveHandler(txtImageName_TextChanged)
-        txtImageInformation1.Inlines.Clear()
-        txtImageInformation2.Inlines.Clear()
-        txtImageName.Text <- ""
+        win.txtImageName.TextChanged.RemoveHandler(txtImageName_TextChanged)
+        win.txtImageInformation1.Inlines.Clear()
+        win.txtImageInformation2.Inlines.Clear()
+        win.txtImageName.Text <- ""
 
         match state.CurrentImage with
         | Some srcImg ->
-            gridImageInformation.Visibility <- Visibility.Visible
-            txtImageName.Text <- srcImg.name
-            txtImageName.TextChanged.AddHandler(txtImageName_TextChanged)
+            win.gridImageInformation.Visibility <- Visibility.Visible
+            win.txtImageName.Text <- srcImg.name
+            win.txtImageName.TextChanged.AddHandler(txtImageName_TextChanged)
 
             // The left part.
             let parasitemiaStr = Utils.percentText (state.ImageParasitemia srcImg)
-            txtImageInformation1.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
-            txtImageInformation1.Inlines.Add(parasitemiaStr)
-            txtImageInformation1.Inlines.Add(Documents.LineBreak())
+            win.txtImageInformation1.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
+            win.txtImageInformation1.Inlines.Add(parasitemiaStr)
+            win.txtImageInformation1.Inlines.Add(Documents.LineBreak())
 
-            txtImageInformation1.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
-            txtImageInformation1.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
+            win.txtImageInformation1.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
+            win.txtImageInformation1.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
 
             // The right part part.
-            txtImageInformation2.Inlines.Add(Documents.Run("Added infected erythrocyte: ", FontWeight = FontWeights.Bold))
-            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg true) + " " + (state.ImageManuallyChangedRBCStr srcImg true)))
-            txtImageInformation2.Inlines.Add(Documents.LineBreak())
-            txtImageInformation2.Inlines.Add(Documents.Run("Removed infected erythrocyte: ", FontWeight = FontWeights.Bold))
-            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg false) + " " + (state.ImageManuallyChangedRBCStr srcImg false)))
+            win.txtImageInformation2.Inlines.Add(Documents.Run("Added infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            win.txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg true) + " " + (state.ImageManuallyChangedRBCStr srcImg true)))
+            win.txtImageInformation2.Inlines.Add(Documents.LineBreak())
+            win.txtImageInformation2.Inlines.Add(Documents.Run("Removed infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            win.txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg false) + " " + (state.ImageManuallyChangedRBCStr srcImg false)))
 
         | _ ->
-            gridImageInformation.Visibility <- Visibility.Hidden
+            win.gridImageInformation.Visibility <- Visibility.Hidden
 
     let updateGlobalParasitemia () =
-        txtGlobalParasitemia.Inlines.Clear()
+        win.txtGlobalParasitemia.Inlines.Clear()
         let total, infected = state.GlobalParasitemia
-        txtGlobalParasitemia.Inlines.Add(Documents.Run(Utils.percentText (total, infected), FontWeight = FontWeights.Bold))
+        win.txtGlobalParasitemia.Inlines.Add(Documents.Run(Utils.percentText (total, infected), FontWeight = FontWeights.Bold))
         if total > 0 && total < warningBelowNumberOfRBC
         then
-            txtGlobalParasitemia.Inlines.Add(
+            win.txtGlobalParasitemia.Inlines.Add(
                 Documents.Run(
                     sprintf " Warning: the number of erythrocytes should be above %d" warningBelowNumberOfRBC,
                     FontWeight = FontWeights.Bold,
                     Foreground = Brushes.Red))
 
     let updateViewportPreview () =
-        for preview in stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> do
+        for preview in win.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 canvasWidth = win.canvasCurrentImage.ActualWidth * currentScale
+                let canvasHeight = win.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.
+                let marginLeft = previewWidth * (win.scrollViewCurrentImage.HorizontalOffset - win.borderCurrentImage.BorderThickness.Left) / canvasWidth - 2.
+                let marginRight = previewWidth * (canvasWidth - (win.scrollViewCurrentImage.HorizontalOffset - win.borderCurrentImage.BorderThickness.Right) - win.scrollViewCurrentImage.ViewportWidth) / canvasWidth - 2.
+                let marginTop = previewHeight * (win.scrollViewCurrentImage.VerticalOffset - win.borderCurrentImage.BorderThickness.Top) / canvasHeight - 2.
+                let marginBottom = previewHeight * (canvasHeight - (win.scrollViewCurrentImage.VerticalOffset - win.borderCurrentImage.BorderThickness.Bottom) - win.scrollViewCurrentImage.ViewportHeight) / canvasHeight - 2.
 
                 preview.viewport.Margin <-
                     Thickness(
@@ -196,7 +160,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
 
     let rec setAsInfected (srcImg: SourceImage) (rbc: RBC) (infected: bool) =
         state.SetAsInfected rbc infected
-        canvasCurrentImage.Children
+        win.canvasCurrentImage.Children
         |> Seq.cast<Views.RBCFrame>
         |> Seq.iter
             (fun frame ->
@@ -233,22 +197,22 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             let mutable currentPreview = 0
             for rbc in srcImg.rbcs |> List.filter (fun rbc -> displayHealthy || rbc.infected) do
                 let previewInfected =
-                    if currentPreview < stackRBC.Children.Count
+                    if currentPreview < win.stackRBC.Children.Count
                     then
-                        RBCFrameFromExisting srcImg rbc (stackRBC.Children.[currentPreview] :?> Views.RBCFrame)
+                        RBCFrameFromExisting srcImg rbc (win.stackRBC.Children.[currentPreview] :?> Views.RBCFrame)
                     else
                         let f = RBCFrame srcImg rbc
                         f.MouseLeftButtonUp.AddHandler(fun obj args -> zoomToRBC (f.Tag :?> RBC))
-                        stackRBC.Children.Add(f) |> ignore
+                        win.stackRBC.Children.Add(f) |> ignore
                         f
 
                 currentPreview <- currentPreview + 1
 
-                previewInfected.Height <- stackRBC.ActualHeight
-                previewInfected.Width <- stackRBC.ActualHeight * rbc.size.Width / rbc.size.Height
+                previewInfected.Height <- win.stackRBC.ActualHeight
+                previewInfected.Width <- win.stackRBC.ActualHeight * rbc.size.Width / rbc.size.Height
                 previewInfected.border.Fill <- ImageBrush(BitmapSourceConvert.ToBitmapSource(extractRBCPreview srcImg.img rbc))
 
-            stackRBC.Children.RemoveRange(currentPreview, stackRBC.Children.Count - currentPreview)
+            win.stackRBC.Children.RemoveRange(currentPreview, win.stackRBC.Children.Count - currentPreview)
         | _ -> ()
 
         updateViewportPreview ()
@@ -259,13 +223,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             let mutable currentCanvas = 0
             for rbc in srcImg.rbcs do
                 let frame =
-                    if currentCanvas < canvasCurrentImage.Children.Count
+                    if currentCanvas < win.canvasCurrentImage.Children.Count
                     then
-                        RBCFrameFromExisting srcImg rbc (canvasCurrentImage.Children.[currentCanvas] :?> Views.RBCFrame)
+                        RBCFrameFromExisting srcImg rbc (win.canvasCurrentImage.Children.[currentCanvas] :?> Views.RBCFrame)
                     else
                         let f = RBCFrame srcImg rbc
                         f.Root.Opacity <- 0.7
-                        canvasCurrentImage.Children.Add(f) |> ignore
+                        win.canvasCurrentImage.Children.Add(f) |> ignore
                         f
 
                 currentCanvas <- currentCanvas + 1
@@ -273,8 +237,8 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 Canvas.SetLeft(frame, rbc.center.X - rbc.size.Width / 2.)
                 Canvas.SetTop(frame, rbc.center.Y - rbc.size.Height / 2.)
 
-            for i in currentCanvas .. canvasCurrentImage.Children.Count - 1 do
-                canvasCurrentImage.Children.[i].Visibility <- Visibility.Hidden
+            for i in currentCanvas .. win.canvasCurrentImage.Children.Count - 1 do
+                win.canvasCurrentImage.Children.[i].Visibility <- Visibility.Hidden
         | _ -> ()
 
     let askDocumentPathToSave () : string option =
@@ -329,26 +293,26 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let updateCurrentImage () =
         match state.CurrentImage with
         | Some srcImg ->
-            imgLogos.Visibility <- Visibility.Collapsed
+            win.imgLogos.Visibility <- Visibility.Collapsed
 
             // Highlight the preview.
-            stackPreviews.Children
+            win.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))
+            win.canvasCurrentImage.Height <- float srcImg.img.Height
+            win.canvasCurrentImage.Width <- float srcImg.img.Width
+            win.canvasCurrentImage.Background <- ImageBrush(BitmapSourceConvert.ToBitmapSource(srcImg.img))
 
             updateRBCFramesCurrent ()
             updateRBCFramesPreview ()
 
         | None ->
-            imgLogos.Visibility <- Visibility.Visible
+            win.imgLogos.Visibility <- Visibility.Visible
 
-            stackRBC.Children.Clear()
-            canvasCurrentImage.Children.Clear()
-            canvasCurrentImage.Background <- canvasCurrentImageColor
+            win.stackRBC.Children.Clear()
+            win.canvasCurrentImage.Children.Clear()
+            win.canvasCurrentImage.Background <- canvasCurrentImageColor
 
         updateCurrentImageInformation ()
 
@@ -362,7 +326,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         let imgCtrl = Views.ImageSourcePreview(Margin = Thickness(3.))
 
         imgCtrl.menuRemoveImage.Click.AddHandler(fun obj args ->
-            stackPreviews.Children.Remove(imgCtrl)
+            win.stackPreviews.Children.Remove(imgCtrl)
             let srcImg = imgCtrl.Tag :?> SourceImage
             let currentRemoved = Some srcImg = state.CurrentImage
             state.RemoveSourceImage srcImg
@@ -373,23 +337,23 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             updateGlobalParasitemia()
 
             // Update image numbers.
-            stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> |> Seq.iter (fun imgPreview -> imgPreview.txtImageNumber.Text <- (imgPreview.Tag :?> SourceImage).num.ToString()))
+            win.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
+        win.stackPreviews.Children.Add(imgCtrl) |> ignore
 
         // Zoom to a mouse position into the control 'imgCtrl'.
         let zoomTo (mousePos: Point) =
-            let canvasW = canvasCurrentImage.ActualWidth * currentScale
-            let canvasH = canvasCurrentImage.ActualHeight * currentScale
+            let canvasW = win.canvasCurrentImage.ActualWidth * currentScale
+            let canvasH = win.canvasCurrentImage.ActualHeight * currentScale
             let centerX = (mousePos.X - imgCtrl.BorderThickness.Left) / (imgCtrl.ActualWidth - imgCtrl.BorderThickness.Left) * canvasW
             let centerY = (mousePos.Y - imgCtrl.BorderThickness.Top) / (imgCtrl.ActualHeight - imgCtrl.BorderThickness.Top) * canvasH
-            scrollViewCurrentImage.ScrollToHorizontalOffset(centerX - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
-            scrollViewCurrentImage.ScrollToVerticalOffset(centerY - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
+            win.scrollViewCurrentImage.ScrollToHorizontalOffset(centerX - win.scrollViewCurrentImage.ViewportWidth / 2. + win.borderCurrentImage.BorderThickness.Left)
+            win.scrollViewCurrentImage.ScrollToVerticalOffset(centerY - win.scrollViewCurrentImage.ViewportHeight / 2. + win.borderCurrentImage.BorderThickness.Top)
 
         imgCtrl.MouseLeftButtonDown.AddHandler(fun obj args ->
             setCurrentImage (state.SourceImages |> Seq.find (fun srcImg -> (srcImg :> Object) = imgCtrl.Tag))
@@ -408,13 +372,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 imgCtrl.ReleaseMouseCapture())
 
     let updatePreviews () =
-        stackPreviews.Children.Clear ()
+        win.stackPreviews.Children.Clear ()
         for srcImg in state.SourceImages do
             addPreview srcImg
         updateCurrentImage ()
 
     let updateGUI () =
-        txtPatient.Text <- state.PatientID
+        win.txtPatient.Text <- state.PatientID
         updatePreviews ()
         updateGlobalParasitemia ()
         updateDocumentStatus ()
@@ -463,16 +427,16 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 Log.Error(ex.ToString())
                 MessageBox.Show(sprintf "The results cannot be exported in '%s'" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
-    txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+    win.txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- win.txtPatient.Text)
 
-    menuExit.Click.AddHandler(fun obj args -> mainWindow.Root.Close())
-    menuSaveFile.Click.AddHandler(fun obj args -> saveCurrentDocument ())
-    menuSaveAsFile.Click.AddHandler(fun obj args -> saveCurrentDocumentAsNewFile ())
-    menuLoadFile.Click.AddHandler(fun obj args -> askLoadFile ())
-    menuNewFile.Click.AddHandler(fun obj args -> newFile ())
-    menuExportResults.Click.AddHandler(fun obj args -> exportResults ())
+    win.menuExit.Click.AddHandler(fun obj args -> win.Root.Close())
+    win.menuSave.Click.AddHandler(fun obj args -> saveCurrentDocument ())
+    win.menuSaveAs.Click.AddHandler(fun obj args -> saveCurrentDocumentAsNewFile ())
+    win.menuOpen.Click.AddHandler(fun obj args -> askLoadFile ())
+    win.menuNew.Click.AddHandler(fun obj args -> newFile ())
+    win.menuExportResults.Click.AddHandler(fun obj args -> exportResults ())
 
-    menuAddSourceImage.Click.AddHandler(fun obj args ->
+    win.menuAddSourceImage.Click.AddHandler(fun obj args ->
         let dialog = OpenFileDialog(Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true)
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
@@ -494,55 +458,55 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             then
                 updateCurrentImage ())
 
-    menuAnalysis.SubmenuOpened.AddHandler(fun obj args -> menuStartAnalysis.IsEnabled <- state.SourceImages.Count() > 0)
+    win.menuAnalysis.SubmenuOpened.AddHandler(fun obj args -> win.menuStartAnalysis.IsEnabled <- state.SourceImages.Count() > 0)
 
-    menuStartAnalysis.Click.AddHandler(fun obj args ->
-        if Analysis.showWindow mainWindow.Root state
+    win.menuStartAnalysis.Click.AddHandler(fun obj args ->
+        if Analysis.showWindow win.Root state
         then
             updateGlobalParasitemia ()
             updateCurrentImage ())
 
-    menuHightlightRBC.Click.AddHandler(fun obj args ->
-        displayHealthy <- menuHightlightRBC.IsChecked
+    win.menuHightlightRBC.Click.AddHandler(fun obj args ->
+        displayHealthy <- win.menuHightlightRBC.IsChecked
         updateRBCFramesPreview ()
         updateRBCFramesCurrent ())
 
-    menuAbout.Click.AddHandler(fun obj args -> About.showWindow mainWindow.Root)
+    win.menuAbout.Click.AddHandler(fun obj args -> About.showWindow win.Root)
 
-    mainWindow.Root.Closing.AddHandler(fun obj args -> askSaveCurrent ())
+    win.Root.Closing.AddHandler(fun obj args -> askSaveCurrent ())
 
     // Zoom on the current image.
     let adjustCurrentImageBorders (deltaX: float) (deltaY: float) =
-        borderCurrentImage.BorderThickness <-
+        win.borderCurrentImage.BorderThickness <-
             Thickness(
-                (scrollViewCurrentImage.ViewportWidth + deltaX) / 2.,
-                (scrollViewCurrentImage.ViewportHeight + deltaY) / 2.,
-                (scrollViewCurrentImage.ViewportWidth + deltaX) / 2.,
-                (scrollViewCurrentImage.ViewportHeight + deltaY) / 2.)
+                (win.scrollViewCurrentImage.ViewportWidth + deltaX) / 2.,
+                (win.scrollViewCurrentImage.ViewportHeight + deltaY) / 2.,
+                (win.scrollViewCurrentImage.ViewportWidth + deltaX) / 2.,
+                (win.scrollViewCurrentImage.ViewportHeight + deltaY) / 2.)
 
-    canvasCurrentImage.SizeChanged.AddHandler(fun obj args ->
+    win.canvasCurrentImage.SizeChanged.AddHandler(fun obj args ->
         let deltaX = args.NewSize.Width - args.PreviousSize.Width
         let deltaY = args.NewSize.Height - args.PreviousSize.Height
         if deltaX > 0.5 || deltaY > 0.5
         then
             adjustCurrentImageBorders 0.0 0.0
             // Center the view at the center of the image initialy.
-            scrollViewCurrentImage.UpdateLayout()
-            scrollViewCurrentImage.ScrollToHorizontalOffset(borderCurrentImage.ActualWidth / 2. - scrollViewCurrentImage.ViewportWidth / 2.)
-            scrollViewCurrentImage.ScrollToVerticalOffset(borderCurrentImage.ActualHeight / 2. - scrollViewCurrentImage.ViewportHeight / 2.))
+            win.scrollViewCurrentImage.UpdateLayout()
+            win.scrollViewCurrentImage.ScrollToHorizontalOffset(win.borderCurrentImage.ActualWidth / 2. - win.scrollViewCurrentImage.ViewportWidth / 2.)
+            win.scrollViewCurrentImage.ScrollToVerticalOffset(win.borderCurrentImage.ActualHeight / 2. - win.scrollViewCurrentImage.ViewportHeight / 2.))
 
-    scrollViewCurrentImage.SizeChanged.AddHandler(fun obj args ->
+    win.scrollViewCurrentImage.SizeChanged.AddHandler(fun obj args ->
         let deltaX = args.NewSize.Width - args.PreviousSize.Width
         let deltaY = args.NewSize.Height - args.PreviousSize.Height
         adjustCurrentImageBorders deltaX deltaY
-        scrollViewCurrentImage.ScrollToHorizontalOffset(scrollViewCurrentImage.HorizontalOffset + deltaX / 8.)
-        scrollViewCurrentImage.ScrollToVerticalOffset(scrollViewCurrentImage.VerticalOffset + deltaY / 8.))
+        win.scrollViewCurrentImage.ScrollToHorizontalOffset(win.scrollViewCurrentImage.HorizontalOffset + deltaX / 8.)
+        win.scrollViewCurrentImage.ScrollToVerticalOffset(win.scrollViewCurrentImage.VerticalOffset + deltaY / 8.))
 
     let mutable maxScale = 4.
     let mutable minScale = 0.25
     let currentImageScaleTransform = ScaleTransform()
-    canvasCurrentImage.LayoutTransform <- currentImageScaleTransform
-    borderCurrentImage.PreviewMouseWheel.AddHandler(fun obj args ->
+    win.canvasCurrentImage.LayoutTransform <- currentImageScaleTransform
+    win.borderCurrentImage.PreviewMouseWheel.AddHandler(fun obj args ->
         let scaleFactor = if args.Delta > 0 then 2.0 else 0.5
         if scaleFactor > 1. && currentScale < maxScale || scaleFactor < 1. && currentScale > minScale
         then
@@ -552,14 +516,14 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 if newScale > maxScale then maxScale elif newScale < minScale then minScale else newScale
             let realScaleFactor = currentScale / previousScale
 
-            let centerX = scrollViewCurrentImage.HorizontalOffset + scrollViewCurrentImage.ViewportWidth / 2. - borderCurrentImage.BorderThickness.Left
-            let centerY = scrollViewCurrentImage.VerticalOffset + scrollViewCurrentImage.ViewportHeight / 2. - borderCurrentImage.BorderThickness.Top
+            let centerX = win.scrollViewCurrentImage.HorizontalOffset + win.scrollViewCurrentImage.ViewportWidth / 2. - win.borderCurrentImage.BorderThickness.Left
+            let centerY = win.scrollViewCurrentImage.VerticalOffset + win.scrollViewCurrentImage.ViewportHeight / 2. - win.borderCurrentImage.BorderThickness.Top
 
             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)
+            win.scrollViewCurrentImage.ScrollToHorizontalOffset(centerX * realScaleFactor - win.scrollViewCurrentImage.ViewportWidth / 2. + win.borderCurrentImage.BorderThickness.Left)
+            win.scrollViewCurrentImage.ScrollToVerticalOffset(centerY * realScaleFactor - win.scrollViewCurrentImage.ViewportHeight / 2. + win.borderCurrentImage.BorderThickness.Top)
 
         args.Handled <- true)
 
@@ -567,70 +531,70 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let mutable scrollStartPosition = Point(0., 0.)
     let mutable scrollStartOffsetX = 0.
     let mutable scrollStartOffsetY = 0.
-    borderCurrentImage.PreviewMouseLeftButtonDown.AddHandler(fun obj args ->
-        scrollStartPosition <- args.GetPosition(scrollViewCurrentImage)
-        scrollStartOffsetX <- scrollViewCurrentImage.HorizontalOffset
-        scrollStartOffsetY <- scrollViewCurrentImage.VerticalOffset
-        borderCurrentImage.Cursor <- Input.Cursors.ScrollAll
-        borderCurrentImage.CaptureMouse() |> ignore
+    win.borderCurrentImage.PreviewMouseLeftButtonDown.AddHandler(fun obj args ->
+        scrollStartPosition <- args.GetPosition(win.scrollViewCurrentImage)
+        scrollStartOffsetX <- win.scrollViewCurrentImage.HorizontalOffset
+        scrollStartOffsetY <- win.scrollViewCurrentImage.VerticalOffset
+        win.borderCurrentImage.Cursor <- Input.Cursors.ScrollAll
+        win.borderCurrentImage.CaptureMouse() |> ignore
         args.Handled <- true)
 
-    borderCurrentImage.PreviewMouseMove.AddHandler(fun obj args ->
-        if borderCurrentImage.IsMouseCaptured
+    win.borderCurrentImage.PreviewMouseMove.AddHandler(fun obj args ->
+        if win.borderCurrentImage.IsMouseCaptured
         then
-            let position = args.GetPosition(scrollViewCurrentImage)
+            let position = args.GetPosition(win.scrollViewCurrentImage)
             let deltaX = scrollStartPosition.X - position.X
             let deltaY = scrollStartPosition.Y - position.Y
-            scrollViewCurrentImage.ScrollToHorizontalOffset(deltaX + scrollStartOffsetX)
-            scrollViewCurrentImage.ScrollToVerticalOffset(deltaY + scrollStartOffsetY)
+            win.scrollViewCurrentImage.ScrollToHorizontalOffset(deltaX + scrollStartOffsetX)
+            win.scrollViewCurrentImage.ScrollToVerticalOffset(deltaY + scrollStartOffsetY)
 
             args.Handled <- true)
 
-    borderCurrentImage.PreviewMouseLeftButtonUp.AddHandler(fun obj args ->
-        if borderCurrentImage.IsMouseCaptured
+    win.borderCurrentImage.PreviewMouseLeftButtonUp.AddHandler(fun obj args ->
+        if win.borderCurrentImage.IsMouseCaptured
         then
-            borderCurrentImage.Cursor <- Input.Cursors.Arrow
-            borderCurrentImage.ReleaseMouseCapture()
+            win.borderCurrentImage.Cursor <- Input.Cursors.Arrow
+            win.borderCurrentImage.ReleaseMouseCapture()
             args.Handled <- true)
 
     // Shortcuts.
     // Save.
-    mainWindow.Root.InputBindings.Add(
+    win.Root.InputBindings.Add(
         Input.KeyBinding(
             FSharp.ViewModule.FunCommand((fun obj -> saveCurrentDocument ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.S, Input.ModifierKeys.Control))) |> ignore
 
     // Save as.
-    mainWindow.Root.InputBindings.Add(
+    win.Root.InputBindings.Add(
         Input.KeyBinding(
             FSharp.ViewModule.FunCommand((fun obj -> saveCurrentDocumentAsNewFile ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.S, Input.ModifierKeys.Control ||| Input.ModifierKeys.Shift))) |> ignore
 
     // Open.
-    mainWindow.Root.InputBindings.Add(
+    win.Root.InputBindings.Add(
         Input.KeyBinding(
             FSharp.ViewModule.FunCommand((fun obj -> askLoadFile ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.O, Input.ModifierKeys.Control))) |> ignore
 
     // New file.
-    mainWindow.Root.InputBindings.Add(
+    win.Root.InputBindings.Add(
         Input.KeyBinding(
             FSharp.ViewModule.FunCommand((fun obj -> newFile ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.N, Input.ModifierKeys.Control))) |> ignore
 
     // Export results.
-    mainWindow.Root.InputBindings.Add(
+    win.Root.InputBindings.Add(
         Input.KeyBinding(
             FSharp.ViewModule.FunCommand((fun obj -> exportResults ()), (fun obj -> true)),
             Input.KeyGesture(Input.Key.E, Input.ModifierKeys.Control))) |> ignore
 
     // Viewport preview.
-    scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
+    win.scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
 
     updateDocumentStatus ()
-    gridImageInformation.Visibility <- Visibility.Hidden
+    win.gridImageInformation.Visibility <- Visibility.Hidden
 
-    mainWindow.Root.Show()
+    win.Root.Show()
 
     match fileToOpen with
     | Some filepath -> loadFile filepath
index 178fe7c..2231bf4 100644 (file)
@@ -74,6 +74,8 @@
     <Compile Include="XAML\ImageSourceSelection.xaml.fs" />
     <Resource Include="XAML\RBCFrame.xaml" />
     <Compile Include="XAML\RBCFrame.xaml.fs" />
+    <Resource Include="XAML\DPICalculatorWindow.xaml" />
+    <Compile Include="XAML\DPICalculatorWindow.xaml.fs" />
     <Resource Include="XAML\AnalysisWindow.xaml" />
     <Compile Include="XAML\AnalysisWindow.xaml.fs" />
     <Resource Include="XAML\AboutWindow.xaml" />
@@ -85,8 +87,9 @@
     <Compile Include="PiaZ.fs" />
     <Compile Include="State.fs" />
     <Compile Include="Export.fs" />
-    <Compile Include="About.fs" />
+    <Compile Include="DPICalculator.fs" />
     <Compile Include="Analysis.fs" />
+    <Compile Include="About.fs" />
     <Compile Include="GUI.fs" />
     <None Include="App.config" />
     <Content Include="packages.config" />
index ed9130d..d81d7e5 100644 (file)
@@ -2,5 +2,5 @@
 
 open FsXaml
 
-type AboutWindow = XAML<"XAML/AboutWindow.xaml">
+type AboutWindow = XAML<"XAML/AboutWindow.xaml", true>
 
index 07d9573..10cd77d 100644 (file)
@@ -2,5 +2,5 @@
 
 open FsXaml
 
-type AnalysisWindow = XAML<"XAML/AnalysisWindow.xaml">
+type AnalysisWindow = XAML<"XAML/AnalysisWindow.xaml", true>
 
diff --git a/Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml b/Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml
new file mode 100644 (file)
index 0000000..e731014
--- /dev/null
@@ -0,0 +1,41 @@
+<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="DPICalculatorWindow" Height="200" Width="378.5" MinHeight="200" MinWidth="280" Title="About" Icon="pack://application:,,,/Resources/icon.ico">
+   <Grid Margin="3">
+      <Grid.ColumnDefinitions>
+         <ColumnDefinition Width="Auto"/>
+         <ColumnDefinition Width="1*"/>
+      </Grid.ColumnDefinitions>
+      <Grid.RowDefinitions>
+         <RowDefinition Height="Auto" />
+         <RowDefinition Height="Auto" />
+         <RowDefinition Height="Auto" />
+         <RowDefinition Height="20" />
+         <RowDefinition Height="Auto" />
+         <RowDefinition Height="1*" />
+      </Grid.RowDefinitions>
+      <Label Content="Sensor size" Grid.Row="0" Grid.Column="0" />
+      <ComboBox x:Name="cmbSensorSize" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" />
+
+      <Label Content="Sensor resolution [Megapixel]" Grid.Row="1" Grid.Column="0" />
+      <TextBox x:Name="txtSensorResolution" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" />
+
+      <Label Content="Zoom" Grid.Row="2" Grid.Column="0" />
+      <TextBox x:Name="txtZoom" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" />
+
+      <Label Content="Image resolution [DPI]" Grid.Row="4" Grid.Column="0" />
+      <TextBox x:Name="txtImageResolution" Grid.Row="4" Grid.Column="1" VerticalAlignment="Center" IsReadOnly="True" />
+
+      <Grid Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2">
+         <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="1*"/>
+            <ColumnDefinition Width="Auto"/>
+         </Grid.ColumnDefinitions>
+         <Button x:Name="butCancel" Content="Cancel" HorizontalAlignment="Right" Margin="3" VerticalAlignment="Bottom" Width="75" Height="20" Grid.Column="0" />
+         <Button x:Name="butOK" Content="OK" HorizontalAlignment="Right" Margin="3" VerticalAlignment="Bottom" Width="75" Height="20" Grid.Column="1" />
+      </Grid>
+   </Grid>
+</Window>
\ No newline at end of file
diff --git a/Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml.fs b/Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml.fs
new file mode 100644 (file)
index 0000000..ef9cbf4
--- /dev/null
@@ -0,0 +1,6 @@
+namespace ParasitemiaUI.Views
+
+open FsXaml
+
+type DPICalculatorWindow = XAML<"XAML/DPICalculatorWindow.xaml", true>
+
index 7958818..94ca3ed 100644 (file)
@@ -39,6 +39,7 @@
             <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
+               <ColumnDefinition Width="Auto"/>
             </Grid.ColumnDefinitions>
             <TextBox x:Name="txtResolution" Margin="3" Text="" Grid.Column="0" />
             <Button x:Name="butDefaultResolutions" Content="Predefined values" Grid.Column="1" Margin="3">
@@ -66,6 +67,7 @@
                   </Style>
                </Button.Style>
             </Button>
+            <Button x:Name="butDPICalculator" Content="DPI calculator" Grid.Column="2" Margin="3" />
          </Grid>
       </Grid>
    </Grid>
index adbfff5..e8c9451 100644 (file)
@@ -2,5 +2,5 @@
 
 open FsXaml
 
-type MainWindow = XAML<"XAML/MainWindow.xaml">
+type MainWindow = XAML<"XAML/MainWindow.xaml", true>