From: Greg Burri Date: Thu, 28 Jan 2016 08:20:51 +0000 (+0100) Subject: Add a DPI calculator to help to find the correct image resolution. X-Git-Tag: 1.0.11~28 X-Git-Url: http://git.euphorik.ch/?a=commitdiff_plain;h=23466ba92c5595f6c0b9f97c86a221a4e5ffebe9;p=master-thesis.git Add a DPI calculator to help to find the correct image resolution. --- diff --git a/Parasitemia/ParasitemiaCore/Analysis.fs b/Parasitemia/ParasitemiaCore/Analysis.fs new file mode 100644 index 0000000..bad0daf --- /dev/null +++ b/Parasitemia/ParasitemiaCore/Analysis.fs @@ -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 + +/// +/// Analyze the given image and detect reb blood cell (RBC) in it. +/// +/// The image +/// The name, used during logging +/// The configuration, must not be shared with another analysis +/// 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 +/// A list of detected cells or nothing if the process has been cancelled +let doAnalysis (img: Image) (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() + + 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() + 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 } + +/// +/// Do multiple analyses on the same time. The number of concurrent process depends if the number of the core. +/// +/// The images: (name * configuration * image) +/// 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 +/// 'None' if the process has been cancelled or the list of result as (name * cells), 'name' corresponds to the given name +let doMultipleAnalysis (imgs: (string * Config * Image) 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() + 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 + diff --git a/Parasitemia/ParasitemiaCore/AssemblyInfo.fs b/Parasitemia/ParasitemiaCore/AssemblyInfo.fs index c129f42..21b305e 100644 --- a/Parasitemia/ParasitemiaCore/AssemblyInfo.fs +++ b/Parasitemia/ParasitemiaCore/AssemblyInfo.fs @@ -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: // [] -[] -[] +[] +[] do () \ No newline at end of file diff --git a/Parasitemia/ParasitemiaCore/Config.fs b/Parasitemia/ParasitemiaCore/Config.fs index 9724d9c..385b7d4 100644 --- a/Parasitemia/ParasitemiaCore/Config.fs +++ b/Parasitemia/ParasitemiaCore/Config.fs @@ -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 // 220.e3 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 index 926aeac..0000000 --- a/Parasitemia/ParasitemiaCore/MainAnalysis.fs +++ /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 - -/// -/// Analyze the given image and detect reb blood cell (RBC) in it. -/// -/// The image -/// The name, used during logging -/// The configuration, must not be shared with another analysis -/// 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 -/// A list of detected cells or nothing if the process has been cancelled -let doAnalysis (img: Image) (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() - - 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() - 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 } - -/// -/// Do multiple analyses on the same time. The number of concurrent process depends if the number of the core. -/// -/// The images: (name * configuration * image) -/// 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 -/// 'None' if the process has been cancelled or the list of result as (name * cells), 'name' corresponds to the given name -let doMultipleAnalysis (imgs: (string * Config * Image) 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() - 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 - diff --git a/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj b/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj index 6a9eb75..473f11d 100644 --- a/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj +++ b/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj @@ -67,7 +67,7 @@ - + diff --git a/Parasitemia/ParasitemiaCore/Types.fs b/Parasitemia/ParasitemiaCore/Types.fs index 8f6c34a..5440115 100644 --- a/Parasitemia/ParasitemiaCore/Types.fs +++ b/Parasitemia/ParasitemiaCore/Types.fs @@ -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 diff --git a/Parasitemia/ParasitemiaCore/UnitsOfMeasure.fs b/Parasitemia/ParasitemiaCore/UnitsOfMeasure.fs index 36c9495..637f49e 100644 --- a/Parasitemia/ParasitemiaCore/UnitsOfMeasure.fs +++ b/Parasitemia/ParasitemiaCore/UnitsOfMeasure.fs @@ -1,12 +1,17 @@ module ParasitemiaCore.UnitsOfMeasure [] type px // Pixel. +[] type mm [] type μm [] type inch [] type ppi = px / inch // Pixel per inch. let μmInchRatio = 25.4e3<μm/inch> +let mmInchRatio = 25.4 let μmToInch(x: float<μm>) : float = x / μmInchRatio let inchToμm(x: float) : float<μm> = x * μmInchRatio +let mmToInch(x: float) : float = x / mmInchRatio +let inchTomm(x: float) : float = x * mmInchRatio + diff --git a/Parasitemia/ParasitemiaUI/About.fs b/Parasitemia/ParasitemiaUI/About.fs index 44dd881..18f266f 100644 --- a/Parasitemia/ParasitemiaUI/About.fs +++ b/Parasitemia/ParasitemiaUI/About.fs @@ -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 diff --git a/Parasitemia/ParasitemiaUI/Analysis.fs b/Parasitemia/ParasitemiaUI/Analysis.fs index 9b9ed51..70d9ef0 100644 --- a/Parasitemia/ParasitemiaUI/Analysis.fs +++ b/Parasitemia/ParasitemiaUI/Analysis.fs @@ -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(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 + let sourceImagesControls = win.stackSourceImagesSelection.Children |> Seq.cast 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) diff --git a/Parasitemia/ParasitemiaUI/AssemblyInfo.fs b/Parasitemia/ParasitemiaUI/AssemblyInfo.fs index b6efc67..46c0c22 100644 --- a/Parasitemia/ParasitemiaUI/AssemblyInfo.fs +++ b/Parasitemia/ParasitemiaUI/AssemblyInfo.fs @@ -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: // [] -[] -[] +[] +[] do () \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/DPICalculator.fs b/Parasitemia/ParasitemiaUI/DPICalculator.fs new file mode 100644 index 0000000..dbd7251 --- /dev/null +++ b/Parasitemia/ParasitemiaUI/DPICalculator.fs @@ -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 + h: float + 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; h = 4.29; txt = "1/2.5″" } + { w = 7.6; h = 5.7; txt = "1/1.7″" } + { w = 8.6; h = 6.6; txt = "2/3″" } + { w = 13.2; h = 8.8; txt = "1″" } + { w = 17.3; h = 13.; txt = "" } + { w = 20.7; h = 13.8; txt = "" } + { w = 22.2; h = 14.8; txt = "" } + { w = 23.6; h = 15.7; txt = "" } + { w = 28.7; h = 19.; txt = "" } + { w = 28.7; h = 19.; txt = "" } ] + + for size in sensorSizes do + win.cmbSensorSize.Items.Add(size) |> ignore + win.cmbSensorSize.SelectedIndex <- 0 + + let resolution (w_p: float) (w_mm: float) (zoom: float) : float = + 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. * 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(false); win.Root.Close()) + win.butOK.Click.AddHandler(fun obj args -> win.Root.DialogResult <- Nullable(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 + diff --git a/Parasitemia/ParasitemiaUI/GUI.fs b/Parasitemia/ParasitemiaUI/GUI.fs index 3746461..8438b4b 100644 --- a/Parasitemia/ParasitemiaUI/GUI.fs +++ b/Parasitemia/ParasitemiaUI/GUI.fs @@ -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) (rbc: RBC) : Emgu.CV.Image = @@ -98,13 +62,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) = frame let updateDocumentStatus () = - txtDocumentStatus.Text <- if state.FilePath = "" then "" else state.FilePath + win.txtDocumentStatus.Text <- if state.FilePath = "" then "" 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 "" 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 "" 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 do + for preview in win.stackPreviews.Children |> Seq.cast 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 |> 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 |> 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 |> Seq.iter (fun imgPreview -> imgPreview.txtImageNumber.Text <- (imgPreview.Tag :?> SourceImage).num.ToString())) + win.stackPreviews.Children |> Seq.cast |> 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 diff --git a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj index 178fe7c..2231bf4 100644 --- a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj +++ b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj @@ -74,6 +74,8 @@ + + @@ -85,8 +87,9 @@ - + + diff --git a/Parasitemia/ParasitemiaUI/XAML/AboutWindow.xaml.fs b/Parasitemia/ParasitemiaUI/XAML/AboutWindow.xaml.fs index ed9130d..d81d7e5 100644 --- a/Parasitemia/ParasitemiaUI/XAML/AboutWindow.xaml.fs +++ b/Parasitemia/ParasitemiaUI/XAML/AboutWindow.xaml.fs @@ -2,5 +2,5 @@ open FsXaml -type AboutWindow = XAML<"XAML/AboutWindow.xaml"> +type AboutWindow = XAML<"XAML/AboutWindow.xaml", true> diff --git a/Parasitemia/ParasitemiaUI/XAML/AnalysisWindow.xaml.fs b/Parasitemia/ParasitemiaUI/XAML/AnalysisWindow.xaml.fs index 07d9573..10cd77d 100644 --- a/Parasitemia/ParasitemiaUI/XAML/AnalysisWindow.xaml.fs +++ b/Parasitemia/ParasitemiaUI/XAML/AnalysisWindow.xaml.fs @@ -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 index 0000000..e731014 --- /dev/null +++ b/Parasitemia/ParasitemiaUI/XAML/DPICalculatorWindow.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + +