From c4a76a01f62568c6353396ff85551a3151fc5236 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Fri, 22 Jan 2016 12:38:55 +0100 Subject: [PATCH] =?utf8?q?Add=20some=20GUI=20elements=20:=20-=20A=20warnin?= =?utf8?q?g=20if=20not=20enough=20erytrocytes=20-=20The=20list=20of=20manu?= =?utf8?q?ally=20modifier=20erytrocytes=20-=20Image=20names=20-=20Exportat?= =?utf8?q?ion=20des=20r=C3=A9sultats?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- Parasitemia/ParasitemiaCore/Ellipse.fs | 6 +- Parasitemia/ParasitemiaCore/ImgTools.fs | 36 +++------ Parasitemia/ParasitemiaCore/MainAnalysis.fs | 2 +- .../ParasitemiaCore/MatchingEllipses.fs | 15 ++-- Parasitemia/ParasitemiaUI/Export.fs | 24 ++++++ Parasitemia/ParasitemiaUI/GUI.fs | 78 +++++++++++++++---- .../ParasitemiaUI/ParasitemiaUI.fsproj | 4 +- Parasitemia/ParasitemiaUI/PiaZ.fs | 12 ++- Parasitemia/ParasitemiaUI/Program.fs | 3 +- Parasitemia/ParasitemiaUI/State.fs | 26 ++++++- Parasitemia/ParasitemiaUI/Types.fs | 2 + Parasitemia/ParasitemiaUI/Utils.fs | 12 +++ .../ParasitemiaUI/XAML/MainWindow.xaml | 30 +++++-- 13 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 Parasitemia/ParasitemiaUI/Export.fs create mode 100644 Parasitemia/ParasitemiaUI/Utils.fs diff --git a/Parasitemia/ParasitemiaCore/Ellipse.fs b/Parasitemia/ParasitemiaCore/Ellipse.fs index e65100b..1059e3b 100644 --- a/Parasitemia/ParasitemiaCore/Ellipse.fs +++ b/Parasitemia/ParasitemiaCore/Ellipse.fs @@ -261,8 +261,8 @@ let private areVectorsValid (p1x: float32) (p1y: float32) (p2x: float32) (p2y: f let find (edges: Matrix) - (xGradient: Image) - (yGradient: Image) + (xGradient: Matrix) + (yGradient: Matrix) (config: Config) : MatchingEllipses = let r1, r2 = config.RBCRadius.Min, config.RBCRadius.Max @@ -334,7 +334,7 @@ let find (edges: Matrix) squaredDistance p1xf p1yf p3xf p3yf >= squaredMinimumDistance && squaredDistance p2xf p2yf p3xf p3yf >= squaredMinimumDistance then - match areVectorsValid (float32 p1xf) (float32 p1yf) (float32 p2xf) (float32 p2yf) -xDirData.[p1.Y, p1.X, 0] -yDirData.[p1.Y, p1.X, 0] -xDirData.[p2.Y, p2.X, 0] -yDirData.[p2.Y, p2.X, 0] with + match areVectorsValid (float32 p1xf) (float32 p1yf) (float32 p2xf) (float32 p2yf) -xDirData.[p1.Y, p1.X] -yDirData.[p1.Y, p1.X] -xDirData.[p2.Y, p2.X] -yDirData.[p2.Y, p2.X] with | Some (m1, m2) -> match ellipse2 p1xf p1yf (float m1) p2xf p2yf (float m2) p3xf p3yf with | Some e when e.Cx > 0.f && e.Cx < w_f - 1.f && e.Cy > 0.f && e.Cy < h_f - 1.f && diff --git a/Parasitemia/ParasitemiaCore/ImgTools.fs b/Parasitemia/ParasitemiaCore/ImgTools.fs index 44a6c86..3fb8ecf 100644 --- a/Parasitemia/ParasitemiaCore/ImgTools.fs +++ b/Parasitemia/ParasitemiaCore/ImgTools.fs @@ -168,31 +168,19 @@ let suppressMAdjacency (img: Matrix) = /// The thresholds are automatically defined with otsu on gradient magnitudes. /// /// -let findEdges (img: Image) : Matrix * Image * Image = +let findEdges (img: Image) : Matrix * Matrix * Matrix = let w = img.Width let h = img.Height use sobelKernel = - new ConvolutionKernelF(array2D [[ 1.0f; 0.0f; -1.0f ] - [ 2.0f; 0.0f; -2.0f ] - [ 1.0f; 0.0f; -1.0f ]], Point(1, 1)) + new Matrix(array2D [[ 1.0f; 0.0f; -1.0f ] + [ 2.0f; 0.0f; -2.0f ] + [ 1.0f; 0.0f; -1.0f ]]) - let xGradient = img.Convolution(sobelKernel) - let yGradient = img.Convolution(sobelKernel.Transpose()) - - let xGradientData = xGradient.Data - let yGradientData = yGradient.Data - for r in 0 .. h - 1 do - xGradientData.[r, 0, 0] <- 0.f - xGradientData.[r, w - 1, 0] <- 0.f - yGradientData.[r, 0, 0] <- 0.f - yGradientData.[r, w - 1, 0] <- 0.f - - for c in 0 .. w - 1 do - xGradientData.[0, c, 0] <- 0.f - xGradientData.[h - 1, c, 0] <- 0.f - yGradientData.[0, c, 0] <- 0.f - yGradientData.[h - 1, c, 0] <- 0.f + let xGradient = new Matrix(img.Size) + let yGradient = new Matrix(img.Size) + CvInvoke.Filter2D(img, xGradient, sobelKernel, Point(1, 1)) + CvInvoke.Filter2D(img, yGradient, sobelKernel.Transpose(), Point(1, 1)) use magnitudes = new Matrix(xGradient.Size) use angles = new Matrix(xGradient.Size) @@ -223,8 +211,8 @@ let findEdges (img: Image) : Matrix * Image for i in 1 .. h - 2 do for j in 1 .. w - 2 do - let vx = xGradientData.[i, j, 0] - let vy = yGradientData.[i, j, 0] + let vx = xGradientData.[i, j] + let vy = yGradientData.[i, j] if vx <> 0.f || vy <> 0.f then let angle = anglesData.[i, j] @@ -942,7 +930,7 @@ let drawLineF (img: Image<'TColor, 'TDepth>) (color: 'TColor) (x0: float) (y0: f let drawEllipse (img: Image<'TColor, 'TDepth>) (e: Ellipse) (color: 'TColor) (alpha: float) = if alpha >= 1.0 then - img.Draw(Emgu.CV.Structure.Ellipse(PointF(float32 e.Cx, float32 e.Cy), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias) + img.Draw(Emgu.CV.Structure.Ellipse(PointF(e.Cx, e.Cy), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias) else let windowPosX = e.Cx - e.A - 5.f let gapX = windowPosX - (float32 (int windowPosX)) @@ -956,7 +944,7 @@ let drawEllipse (img: Image<'TColor, 'TDepth>) (e: Ellipse) (color: 'TColor) (al if roi = img.ROI // We do not display ellipses touching the edges (FIXME) then use i = new Image<'TColor, 'TDepth>(img.ROI.Size) - i.Draw(Emgu.CV.Structure.Ellipse(PointF(float32 <| (e.A + 5.f + gapX) , float32 <| (e.A + 5.f + gapY)), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias) + i.Draw(Emgu.CV.Structure.Ellipse(PointF(e.A + 5.f + gapX, e.A + 5.f + gapY), SizeF(2.f * e.B, 2.f * e.A), e.Alpha / PI * 180.f), color, 1, CvEnum.LineType.AntiAlias) CvInvoke.AddWeighted(img, 1.0, i, alpha, 0.0, img) img.ROI <- Rectangle.Empty diff --git a/Parasitemia/ParasitemiaCore/MainAnalysis.fs b/Parasitemia/ParasitemiaCore/MainAnalysis.fs index 7b8b066..4ab52d9 100644 --- a/Parasitemia/ParasitemiaCore/MainAnalysis.fs +++ b/Parasitemia/ParasitemiaCore/MainAnalysis.fs @@ -50,7 +50,7 @@ let doAnalysis (img: Image) (name: string) (config: Config) (reportPr logWithName "Starting analysis ..." - use green = img.Item(1) + use green = img.[1] let greenFloat = green.Convert() let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation diff --git a/Parasitemia/ParasitemiaCore/MatchingEllipses.fs b/Parasitemia/ParasitemiaCore/MatchingEllipses.fs index c7a0627..3ed8ad6 100644 --- a/Parasitemia/ParasitemiaCore/MatchingEllipses.fs +++ b/Parasitemia/ParasitemiaCore/MatchingEllipses.fs @@ -9,6 +9,12 @@ open System.Collections.Generic open Types open Utils +// All ellipses with a score below this are removed. +let matchingScoreThreshold = 0.4f +let matchingScorePower = 20.f +let windowSizeRadiusFactor = 1.f / 2.f +let minimumDistanceFromCenterRadiusFactor = 1.f / 3.f + type private EllipseScoreFlaggedKd (matchingScore: float32, e: Ellipse) = let mutable matchingScore = matchingScore @@ -29,9 +35,6 @@ type private EllipseScoreFlaggedKd (matchingScore: float32, e: Ellipse) = type MatchingEllipses (radius: float32) = let ellipses = List() - // All ellipses with a score below this are removed. - let matchingScoreThreshold = 0.4f - member this.Add (e: Ellipse) = ellipses.Add(EllipseScoreFlaggedKd(0.f, e)) @@ -47,7 +50,7 @@ type MatchingEllipses (radius: float32) = let tree = KdTree.Tree.BuildTree (List.ofSeq ellipses) // 2) Compute the matching score of each ellipses. - let windowSize = radius / 3.f + let windowSize = radius * windowSizeRadiusFactor for e in ellipses do e.Processed <- true let areaE = e.Ellipse.Area @@ -63,7 +66,7 @@ type MatchingEllipses (radius: float32) = | Some (overlapArea, _, _) // Because of approximation error, see https://github.com/chraibi/EEOver/issues/4 when overlapArea - areaE < 1.f && overlapArea - areaOther < 1.f -> - let matchingScore = (2.f * overlapArea / (areaE + areaOther)) ** 30.f + let matchingScore = (2.f * overlapArea / (areaE + areaOther)) ** matchingScorePower other.AddMatchingScore(matchingScore) e.AddMatchingScore(matchingScore) | _ -> () @@ -82,7 +85,7 @@ type MatchingEllipses (radius: float32) = if not other.Removed && e.MatchingScore > other.MatchingScore then // Case where ellipses are too close. - if distanceTwoPoints (PointF(e.Ellipse.Cx, e.Ellipse.Cy)) (PointF(other.Ellipse.Cx, other.Ellipse.Cy)) < 0.3f * e.Ellipse.B + if distanceTwoPoints (PointF(e.Ellipse.Cx, e.Ellipse.Cy)) (PointF(other.Ellipse.Cx, other.Ellipse.Cy)) < minimumDistanceFromCenterRadiusFactor * e.Ellipse.B then other.Removed <- true else diff --git a/Parasitemia/ParasitemiaUI/Export.fs b/Parasitemia/ParasitemiaUI/Export.fs new file mode 100644 index 0000000..adecd6c --- /dev/null +++ b/Parasitemia/ParasitemiaUI/Export.fs @@ -0,0 +1,24 @@ +module ParasitemiaUI.Export + +open System +open System.IO + +open State + +/// If the results cannot be exported +let exportResults (state: State) (filePath: string) = + use writer = new StreamWriter(new FileStream(filePath, FileMode.Create, FileAccess.Write)) + fprintfn writer "File: %s" state.FilePath + fprintfn writer "Export date: %A" DateTime.Now + + fprintfn writer "" + fprintfn writer "Patient ID: %s" state.PatientID + fprintfn writer "Global parasitemia: %s" (Utils.percentText state.GlobalParasitemia) + + for srcImg in state.SourceImages do + fprintfn writer "" + fprintfn writer "Image name: %s" srcImg.name + fprintfn writer "Parasitemia: %s" (Utils.percentText (state.ImageParasitemia srcImg)) + fprintfn writer "Added infected erythrocyte: %s %s" (state.ImageNbManuallyChangedRBCStr srcImg true) (state.ImageManuallyChangedRBCStr srcImg true) + fprintfn writer "Removed infected erythrocyte: %s %s" (state.ImageNbManuallyChangedRBCStr srcImg false) (state.ImageManuallyChangedRBCStr srcImg false) + () \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/GUI.fs b/Parasitemia/ParasitemiaUI/GUI.fs index 2ff3116..d749479 100644 --- a/Parasitemia/ParasitemiaUI/GUI.fs +++ b/Parasitemia/ParasitemiaUI/GUI.fs @@ -27,12 +27,14 @@ let run (defaultConfig: Config) (fileToOpen: string option) = let state = State.State() 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" @@ -44,15 +46,18 @@ let run (defaultConfig: Config) (fileToOpen: string option) = let txtMessageStatus: TextBlock = ctrl "txtMessageStatus" let txtPatient: TextBox = ctrl "txtPatient" - let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia" + 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" @@ -117,21 +122,24 @@ let run (defaultConfig: Config) (fileToOpen: string option) = scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left) scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top) - let percentText (nbTotal: int, nb: int) : string = - if nbTotal = 0 - then - "" - else - let percent = 100. * (float nb) / (float nbTotal) - sprintf "%.1f %% (%d / %d)" percent nb nbTotal + + let txtImageName_TextChanged = + TextChangedEventHandler(fun obj args -> state.CurrentImage |> Option.iter(fun srcImg -> state.SetName srcImg txtImageName.Text)) let updateCurrentImageInformation () = + txtImageName.TextChanged.RemoveHandler(txtImageName_TextChanged) txtImageInformation1.Inlines.Clear() txtImageInformation2.Inlines.Clear() + txtImageName.Text <- "" match state.CurrentImage with | Some srcImg -> - let parasitemiaStr = percentText (state.ImageParasitemia srcImg) + gridImageInformation.Visibility <- Visibility.Visible + txtImageName.Text <- srcImg.name + 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()) @@ -139,17 +147,27 @@ let run (defaultConfig: Config) (fileToOpen: string option) = 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())) - let alteredStr = percentText (state.ImageNbAltered srcImg) - txtImageInformation2.Inlines.Add(Documents.Run("Number of erytrocytes manually altered: ", FontWeight = FontWeights.Bold)) - txtImageInformation2.Inlines.Add(Documents.Run(alteredStr)) + // 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))) - txtImageInformation2.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold)) - txtImageInformation2.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString())) - | _ -> () + | _ -> + gridImageInformation.Visibility <- Visibility.Hidden let updateGlobalParasitemia () = - txtGlobalParasitemia.Text <- percentText state.GlobalParasitemia + txtGlobalParasitemia.Inlines.Clear() + let total, infected = state.GlobalParasitemia + txtGlobalParasitemia.Inlines.Add(Documents.Run(Utils.percentText (total, infected), FontWeight = FontWeights.Bold)) + if total > 0 && total < warningBelowNumberOfRBC + then + txtGlobalParasitemia.Inlines.Add( + Documents.Run( + sprintf " Warning: the number of erytrocytes should be above %d" warningBelowNumberOfRBC, + FontWeight = FontWeights.Bold, + Foreground = Brushes.Red)) let updateViewportPreview () = for preview in stackPreviews.Children |> Seq.cast do @@ -425,6 +443,26 @@ let run (defaultConfig: Config) (fileToOpen: string option) = state.Reset() updateGUI() + let exportResults () = + let extension = ".txt" + let dialog = SaveFileDialog(AddExtension = true, DefaultExt = extension) + + if state.FilePath <> "" + then + dialog.FileName <- Path.GetFileNameWithoutExtension(state.FilePath) + extension + elif state.PatientID <> "" + then + dialog.FileName <- state.PatientID + extension + + let res = dialog.ShowDialog() + if res.HasValue && res.Value then + try + Export.exportResults state dialog.FileName + with + | :? IOException as ex -> + 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) menuExit.Click.AddHandler(fun obj args -> mainWindow.Root.Close()) @@ -432,6 +470,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) = 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 ()) menuAddSourceImage.Click.AddHandler(fun obj args -> let dialog = OpenFileDialog(Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true) @@ -579,10 +618,17 @@ let run (defaultConfig: Config) (fileToOpen: string option) = FSharp.ViewModule.FunCommand((fun obj -> newFile ()), (fun obj -> true)), Input.KeyGesture(Input.Key.N, Input.ModifierKeys.Control))) |> ignore + // Export results. + mainWindow.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 ()) updateDocumentStatus () + gridImageInformation.Visibility <- Visibility.Hidden mainWindow.Root.Show() diff --git a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj index 483888f..178fe7c 100644 --- a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj +++ b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj @@ -68,8 +68,6 @@ - - @@ -83,8 +81,10 @@ + + diff --git a/Parasitemia/ParasitemiaUI/PiaZ.fs b/Parasitemia/ParasitemiaUI/PiaZ.fs index c9dfd5b..92397be 100644 --- a/Parasitemia/ParasitemiaUI/PiaZ.fs +++ b/Parasitemia/ParasitemiaUI/PiaZ.fs @@ -26,6 +26,8 @@ type JSONInformation = { // Information associated to each images. type JSONSourceImage = { num: int + name: string + RBCRadius: float32 // The RBC Radius found by granulometry. parameters: ParasitemiaCore.Config.Parameters dateLastAnalysis: DateTime @@ -42,7 +44,7 @@ type DocumentData = { let mainEntryName = "info.json" let imageExtension = ".tiff" -let currentFileVersion = 1 +let currentFileVersion = 2 /// /// Save a document in a give file path. The file may already exist. @@ -72,6 +74,7 @@ let save (filePath: string) (data: DocumentData) = imgJSONFileWriter.Write( JsonConvert.SerializeObject( { num = srcImg.num + name = srcImg.name RBCRadius = srcImg.config.RBCRadius.Pixel parameters = srcImg.config.Parameters dateLastAnalysis = srcImg.dateLastAnalysis @@ -108,12 +111,13 @@ let load (filePath: string) : DocumentData = use bitmap = new System.Drawing.Bitmap(imgEntry.Open(), false) let img = new Image(bitmap) imgNum <- imgNum + 1 - let imgEntry = file.GetEntry(imgEntry.Name + ".json") - use imgEntryFileReader = new StreamReader(imgEntry.Open()) - let imgInfo = JsonConvert.DeserializeObject(imgEntryFileReader.ReadToEnd()) + let imgJSONEntry = file.GetEntry(imgEntry.Name + ".json") + use imgJSONFileReader = new StreamReader(imgJSONEntry.Open()) + let imgInfo = JsonConvert.DeserializeObject(imgJSONFileReader.ReadToEnd()) let config = ParasitemiaCore.Config.Config(imgInfo.parameters) config.SetRBCRadius imgInfo.RBCRadius yield { num = imgNum + name = imgInfo.name config = config dateLastAnalysis = imgInfo.dateLastAnalysis img = img diff --git a/Parasitemia/ParasitemiaUI/Program.fs b/Parasitemia/ParasitemiaUI/Program.fs index 24e33cd..feab9ec 100644 --- a/Parasitemia/ParasitemiaUI/Program.fs +++ b/Parasitemia/ParasitemiaUI/Program.fs @@ -77,7 +77,8 @@ let main args = match ParasitemiaCore.Analysis.doMultipleAnalysis images None with | Some results -> for id, cells in results do - let config = images |> List.pick (fun (id', config', _) -> if id' = id then Some config' else None) + let config, img = images |> List.pick (fun (id', config', img') -> if id' = id then Some (config', img') else None) + img.Dispose() let total, infected = countCells cells fprintf resultFile "File: %s %d %d %.2f (diameter: %A)\n" id total infected (100. * (float infected) / (float total)) config.RBCRadius | None -> diff --git a/Parasitemia/ParasitemiaUI/State.fs b/Parasitemia/ParasitemiaUI/State.fs index 63b9bbf..96dfc24 100644 --- a/Parasitemia/ParasitemiaUI/State.fs +++ b/Parasitemia/ParasitemiaUI/State.fs @@ -30,9 +30,24 @@ type State () = List.length srcImg.rbcs, srcImg.rbcs |> List.fold (fun nbInfected rbc -> if rbc.infected then nbInfected + 1 else nbInfected) 0 - member this.ImageNbAltered (srcImg: SourceImage) : int * int = + member this.ImageNbManuallyChangedRBC (srcImg: SourceImage) (setAsInfected: bool) : int * int = List.length srcImg.rbcs, - srcImg.rbcs |> List.fold (fun nbAltered rbc -> if rbc.setManually then nbAltered + 1 else nbAltered) 0 + srcImg.rbcs |> List.fold (fun nb rbc -> if rbc.setManually && rbc.infected = setAsInfected then nb + 1 else nb) 0 + + member this.ImageNbManuallyChangedRBCStr (srcImg: SourceImage) (setAsInfected: bool) : string = + Utils.percentText (this.ImageNbManuallyChangedRBC srcImg setAsInfected) + + member this.ImageManuallyChangedRBC (srcImg: SourceImage) (setAsInfected: bool) : int seq = + query { + for rbc in srcImg.rbcs do + where (rbc.setManually && rbc.infected = setAsInfected) + select rbc.num } + + member this.ImageManuallyChangedRBCStr (srcImg: SourceImage) (setAsInfected: bool) : string = + let listStr = Utils.listAsStr <| this.ImageManuallyChangedRBC srcImg setAsInfected + if listStr = "" + then "" + else "[" + listStr + "]" member this.GlobalParasitemia : int * int = sourceImages @@ -75,6 +90,7 @@ type State () = member this.AddSourceImage (filePath: string) (defaultConfig: ParasitemiaCore.Config.Config) : SourceImage = let srcImg = { num = sourceImages.Count + 1 + name = System.IO.FileInfo(filePath).Name config = defaultConfig.Copy() dateLastAnalysis = DateTime(0L) rbcs = [] @@ -103,6 +119,12 @@ type State () = // Re-numbered the images. sourceImages |> Seq.iteri (fun i srcImg -> srcImg.num <- i + 1) + member this.SetName (srcImg: SourceImage) (name: string) = + if name <> srcImg.name + then + srcImg.name <- name + alteredSinceLastSave <- true + member this.SetResult (imgNum: int) (cells: ParasitemiaCore.Types.Cell list) = let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = imgNum) diff --git a/Parasitemia/ParasitemiaUI/Types.fs b/Parasitemia/ParasitemiaUI/Types.fs index 5de71e6..92d39b8 100644 --- a/Parasitemia/ParasitemiaUI/Types.fs +++ b/Parasitemia/ParasitemiaUI/Types.fs @@ -22,6 +22,8 @@ type RBC = { type SourceImage = { mutable num: int + mutable name: string + mutable config: ParasitemiaCore.Config.Config mutable dateLastAnalysis: DateTime // UTC. img: Image diff --git a/Parasitemia/ParasitemiaUI/Utils.fs b/Parasitemia/ParasitemiaUI/Utils.fs new file mode 100644 index 0000000..324e5f0 --- /dev/null +++ b/Parasitemia/ParasitemiaUI/Utils.fs @@ -0,0 +1,12 @@ +module ParasitemiaUI.Utils + +let listAsStr (s: 'a seq) = + s |> Seq.fold (fun acc obj -> acc + (if acc = "" then "" else ", ") + obj.ToString()) "" + +let percentText (nbTotal: int, nb: int) : string = + if nbTotal = 0 + then + "" + else + let percent = 100. * (float nb) / (float nbTotal) + sprintf "%.1f %% (%d / %d)" percent nb nbTotal \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml b/Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml index e763bea..000da4e 100644 --- a/Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml +++ b/Parasitemia/ParasitemiaUI/XAML/MainWindow.xaml @@ -12,16 +12,18 @@ + + - + - + - + @@ -57,7 +59,9 @@