From ee562c3b613791fd89a5c322546229e9d364cafb Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Thu, 14 Jan 2016 19:00:23 +0100 Subject: [PATCH] The images to be analyzed can be selected. --- Parasitemia/Parasitemia/Config.fs | 40 +++--- Parasitemia/Parasitemia/GUI/Analysis.fs | 117 +++++++++++++---- .../Parasitemia/GUI/AnalysisWindow.xaml | 10 +- Parasitemia/Parasitemia/GUI/GUI.fs | 10 +- .../Parasitemia/GUI/ImageSourcePreview.xaml | 2 +- .../Parasitemia/GUI/ImageSourceSelection.xaml | 72 +++++++++++ .../GUI/ImageSourceSelection.xaml.fs | 17 +++ Parasitemia/Parasitemia/GUI/MainWindow.xaml | 4 +- Parasitemia/Parasitemia/GUI/PiaZ.fs | 121 +++++++----------- Parasitemia/Parasitemia/GUI/State.fs | 13 +- Parasitemia/Parasitemia/GUI/Types.fs | 2 +- Parasitemia/Parasitemia/MainAnalysis.fs | 57 ++++++--- Parasitemia/Parasitemia/Parasitemia.fsproj | 11 +- Parasitemia/Parasitemia/ParasitesMarker.fs | 6 +- Parasitemia/Parasitemia/Program.fs | 6 +- Parasitemia/Parasitemia/UnitsOfMeasure.fs | 16 +-- Parasitemia/Parasitemia/packages.config | 1 + 17 files changed, 329 insertions(+), 176 deletions(-) create mode 100644 Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml create mode 100644 Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml.fs diff --git a/Parasitemia/Parasitemia/Config.fs b/Parasitemia/Parasitemia/Config.fs index 9c23d83..e28398e 100644 --- a/Parasitemia/Parasitemia/Config.fs +++ b/Parasitemia/Parasitemia/Config.fs @@ -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 = μm.ConvertToInch(param.rbcDiameter) / 2. - let rbcRadiusPx: float = param.resolution * rbcRadiusInch + let rbcRadiusInch: float = (μmToInch parameters.rbcDiameter) / 2. + let rbcRadiusPx: float = 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 = μm.ConvertToInch(param.LPFStandardDeviation) * param.resolution + let stdDeviation: float = (μ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. * (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 diff --git a/Parasitemia/Parasitemia/GUI/Analysis.fs b/Parasitemia/Parasitemia/GUI/Analysis.fs index acb9ce2..ccca1b4 100644 --- a/Parasitemia/Parasitemia/GUI/Analysis.fs +++ b/Parasitemia/Parasitemia/GUI/Analysis.fs @@ -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. } + | 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(srcImg.dateLastAnalysis.Ticks = 0L) + imageSourceSelection.lblDateLastAnalysis.Content <- if srcImg.dateLastAnalysis.Ticks = 0L then "" 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(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 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 diff --git a/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml b/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml index 6da0675..7684ad9 100644 --- a/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml +++ b/Parasitemia/Parasitemia/GUI/AnalysisWindow.xaml @@ -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"> - + - + - - + + diff --git a/Parasitemia/Parasitemia/GUI/GUI.fs b/Parasitemia/Parasitemia/GUI/GUI.fs index a7bed54..b0af4cc 100644 --- a/Parasitemia/Parasitemia/GUI/GUI.fs +++ b/Parasitemia/Parasitemia/GUI/GUI.fs @@ -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 ()) diff --git a/Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml b/Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml index 1064059..ae8f5dd 100644 --- a/Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml +++ b/Parasitemia/Parasitemia/GUI/ImageSourcePreview.xaml @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml b/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml new file mode 100644 index 0000000..4fcde66 --- /dev/null +++ b/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 index 0000000..1ca5c66 --- /dev/null +++ b/Parasitemia/Parasitemia/GUI/ImageSourceSelection.xaml.fs @@ -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() *) + +(* type ImageSourcePreviewViewModel() = + inherit ViewModelBase() *) diff --git a/Parasitemia/Parasitemia/GUI/MainWindow.xaml b/Parasitemia/Parasitemia/GUI/MainWindow.xaml index b24b48c..9fa8316 100644 --- a/Parasitemia/Parasitemia/GUI/MainWindow.xaml +++ b/Parasitemia/Parasitemia/GUI/MainWindow.xaml @@ -42,8 +42,8 @@ -