From: Greg Burri Date: Mon, 11 Jan 2016 17:01:39 +0000 (+0100) Subject: * Add the possibility to set an RBC as healthy or infected X-Git-Tag: 1.0.11~57 X-Git-Url: http://git.euphorik.ch/index.cgi?a=commitdiff_plain;ds=sidebyside;h=8013d6a586604e443332e6e9a715c86df444a875;p=master-thesis.git * Add the possibility to set an RBC as healthy or infected --- diff --git a/Parasitemia/Parasitemia/Classifier.fs b/Parasitemia/Parasitemia/Classifier.fs index 3a679e7..1a22129 100644 --- a/Parasitemia/Parasitemia/Classifier.fs +++ b/Parasitemia/Parasitemia/Classifier.fs @@ -229,5 +229,6 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img: Some { cellClass = cellClass center = Point(roundInt e.Cx, roundInt e.Cy) + infectedArea = infectedPixels.Count stainArea = stainPixels elements = elements }) diff --git a/Parasitemia/Parasitemia/GUI/GUI.fs b/Parasitemia/Parasitemia/GUI/GUI.fs index 2aadefe..8aef251 100644 --- a/Parasitemia/Parasitemia/GUI/GUI.fs +++ b/Parasitemia/Parasitemia/GUI/GUI.fs @@ -27,6 +27,7 @@ let run (defaultConfig: Config) = let colorRBCInfected = Brushes.Red let state = State.State() + let mutable currentScale = 1. let exit: MenuItem = ctrl "menuExit" let saveFile: MenuItem = ctrl "menuSave" @@ -35,15 +36,157 @@ let run (defaultConfig: Config) = let addSourceImage: MenuItem = ctrl "menuAddSourceImage" let txtPatient: TextBox = ctrl "txtPatient" - let stackPreviews: StackPanel = ctrl "stackPreviews" + let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia" + let butStartAnalysis: Button = ctrl "butStartAnalysis" + let stackPreviews: StackPanel = ctrl "stackPreviews" + let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage" let borderCurrentImage: Border = ctrl "borderCurrentImage" let canvasCurrentImage: Canvas = ctrl "canvasCurrentImage" + let txtImageInformation: TextBlock = ctrl "txtImageInformation" + + let scrollRBC: ScrollViewer = ctrl "scrollRBC" + let stackRBC: StackPanel = ctrl "stackRBC" // Initializations. + // Utils. + let extractRBCPreview (img: Emgu.CV.Image) (rbc: RBC) : Emgu.CV.Image = + let rbcWidth = rbc.size.Width + let rbcHeight = rbc.size.Height + img.GetSubRect(System.Drawing.Rectangle(System.Drawing.Point(rbc.center.X - rbcWidth / 2. |> Utils.roundInt, rbc.center.Y - rbcHeight / 2. |> Utils.roundInt), + System.Drawing.Size(Utils.roundInt rbcWidth, Utils.roundInt rbcHeight))) + + let setRBCFrameStyle (rbc: RBC) (frame: Views.RBCFrame) = + frame.Opacity <- if rbc.setManually || rbc.infected then 1. else 0. + let color = if rbc.infected then colorRBCInfected else colorRBCHealthy + frame.manuallyAdded.Visibility <- if rbc.setManually then Visibility.Visible else Visibility.Hidden + frame.manuallyAdded.Fill <- color + frame.border.Stroke <- color + + let RBCFrameFromExisting (rbc: RBC) (frame: Views.RBCFrame) : Views.RBCFrame = + frame.Visibility <- Visibility.Visible + frame.Height <- rbc.size.Height + frame.Width <- rbc.size.Width + frame.Tag <- rbc + setRBCFrameStyle rbc frame + frame.border.StrokeThickness <- 1. + frame.lblRBCNumber.Content <- rbc.num + frame + + let highlightRBCFrame (frame: Views.RBCFrame) (highlight: bool) = + let rbc = frame.Tag :?> RBC + if highlight + then + frame.border.StrokeThickness <- 3. + if not rbc.infected && not rbc.setManually then frame.Opacity <- 1. + else + frame.border.StrokeThickness <- 1. + if not rbc.infected && not rbc.setManually 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) + + let parasitemiaText (nbTotal: int, nbInfected: int) : string = + if nbTotal = 0 + then + "" + else + let percent = 100. * (float nbInfected) / (float nbTotal) + sprintf "%.2f %% (%d / %d)" percent nbInfected nbTotal + + let updateCurrentImageInformation () = + match state.CurrentImage with + | Some srcImg -> + let parasitemiaStr = parasitemiaText (state.ImageParasitemia srcImg) + txtImageInformation.Text <- sprintf "Parasitemia: %s" parasitemiaStr + | _ -> () + + let updateGlobalParasitemia () = + txtGlobalParasitemia.Text <- parasitemiaText state.GlobalParasitemia + + let rec setAsInfected (rbc: RBC) (infected: bool) = + rbc.SetAsInfected infected + canvasCurrentImage.Children + |> Seq.cast + |> Seq.iter + (fun frame -> + if (frame.Tag :?> RBC) = rbc + then + setRBCFrameStyle rbc frame) + updateRBCFramesPreview () + updateCurrentImageInformation () + updateGlobalParasitemia () + + and RBCFrame (rbc: RBC) : Views.RBCFrame = + let f = RBCFrameFromExisting rbc (Views.RBCFrame()) + f.menuRBCSetAsHealthy.Click.AddHandler(fun obj args -> setAsInfected (f.Tag :?> RBC) false) + f.menuRBCSetAsInfected.Click.AddHandler(fun obj args -> setAsInfected (f.Tag :?> RBC) true) + f.ContextMenuOpening.AddHandler( + fun obj args -> + if (f.Tag :?> RBC).infected + then + f.menuRBCSetAsHealthy.Visibility <- Visibility.Visible + f.menuRBCSetAsInfected.Visibility <- Visibility.Collapsed + else + f.menuRBCSetAsHealthy.Visibility <- Visibility.Collapsed + f.menuRBCSetAsInfected.Visibility <- Visibility.Visible) + + f.ContextMenuClosing.AddHandler(fun obj args -> if not f.IsMouseOver then highlightRBCFrame f false ) + f.MouseEnter.AddHandler(fun obj args -> highlightRBCFrame f true) + f.MouseLeave.AddHandler(fun obj args -> if not f.grid.ContextMenu.IsOpen then highlightRBCFrame f false) + f + + and updateRBCFramesPreview () = + match state.CurrentImage with + | Some srcImg -> + let mutable currentPreview = 0 + for rbc in srcImg.rbcs |> List.filter (fun rbc -> rbc.infected) do + let previewInfected = + if currentPreview < stackRBC.Children.Count + then + RBCFrameFromExisting rbc (stackRBC.Children.[currentPreview] :?> Views.RBCFrame) + else + let f = RBCFrame rbc + f.MouseLeftButtonUp.AddHandler(fun obj args -> zoomToRBC ((obj :?> Views.RBCFrame).Tag :?> RBC)) + stackRBC.Children.Add(f) |> ignore + f + + currentPreview <- currentPreview + 1 + + previewInfected.Height <- stackRBC.ActualHeight + previewInfected.Width <- 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) + | _ -> () + + let updateRBCFramesCurrent () = + match state.CurrentImage with + | Some srcImg -> + let mutable currentCanvas = 0 + for rbc in srcImg.rbcs do + let frame = + if currentCanvas < canvasCurrentImage.Children.Count + then + RBCFrameFromExisting rbc (canvasCurrentImage.Children.[currentCanvas] :?> Views.RBCFrame) + else + let f = RBCFrame rbc + canvasCurrentImage.Children.Add(f) |> ignore + f + + currentCanvas <- currentCanvas + 1 + + 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 + | _ -> () + // Operations. let synchronizeState () = state.PatientID <- txtPatient.Text @@ -60,33 +203,10 @@ let run (defaultConfig: Config) = canvasCurrentImage.Width <- float srcImg.img.Width canvasCurrentImage.Background <- ImageBrush(BitmapSourceConvert.ToBitmapSource(srcImg.img)) - // Remove all image canvas children and add the RBC. - canvasCurrentImage.Children.Clear() - for rbc in srcImg.rbcs do - let rectangle = - Rectangle( - Height = rbc.size.Height, - Width = rbc.size.Width, - Stroke = (if rbc.infected then colorRBCInfected else colorRBCHealthy), - StrokeThickness = 1., - Fill = SolidColorBrush(Color.FromArgb(0uy, 0uy, 0uy, 0uy)), - Tag = rbc, - Opacity = if rbc.infected then 1. else 0.) - Canvas.SetLeft(rectangle, rbc.center.X - rbc.size.Width / 2.) - Canvas.SetTop(rectangle, rbc.center.Y - rbc.size.Height / 2.) - canvasCurrentImage.Children.Add(rectangle) |> ignore - rectangle.MouseEnter.AddHandler( - fun obj args -> match obj with - | :? Rectangle as r -> - r.StrokeThickness <- 3. - if not (r.Tag :?> RBC).infected then r.Opacity <- 1. - | _ -> ()) - rectangle.MouseLeave.AddHandler( - fun obj args -> match obj with - | :? Rectangle as r -> - r.StrokeThickness <- 1. - if not (r.Tag :?> RBC).infected then r.Opacity <- 0. - | _ -> ()) + updateRBCFramesCurrent () + updateRBCFramesPreview () + updateCurrentImageInformation () + let addPreview (srcImg: SourceImage) = let imgCtrl = Views.ImageSourcePreview(Margin = Thickness(3.)) @@ -103,11 +223,14 @@ let run (defaultConfig: Config) = then for srcImg in state.SourceImages do addPreview srcImg - setCurrentImage (state.SourceImages.First()) + match state.CurrentImage with + | Some srcImg -> setCurrentImage srcImg + | _ -> () let updateGUI () = txtPatient.Text <- state.PatientID updatePreviews () + updateGlobalParasitemia () exit.Click.AddHandler(fun obj args -> mainWindow.Root.Close()) saveFile.Click.AddHandler(fun obj args -> @@ -145,6 +268,7 @@ let run (defaultConfig: Config) = then let srcImg = state.AddSourceImage(dialog.FileName) addPreview srcImg + updateGlobalParasitemia () if state.SourceImages.Count() = 1 then setCurrentImage srcImg) @@ -153,7 +277,12 @@ let run (defaultConfig: Config) = let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig for id, cells in results do state.SetResult (int id) cells - ) + updateGlobalParasitemia () + + // Refresh current image. + match state.CurrentImage with + | Some srcImg -> setCurrentImage srcImg + | _ -> ()) // Zoom on the current image. let adjustCurrentImageBorders (deltaX: float) (deltaY: float) = @@ -182,7 +311,6 @@ let run (defaultConfig: Config) = scrollViewCurrentImage.ScrollToHorizontalOffset(scrollViewCurrentImage.HorizontalOffset + deltaX / 8.) scrollViewCurrentImage.ScrollToVerticalOffset(scrollViewCurrentImage.VerticalOffset + deltaY / 8.)) - let mutable currentScale = 1. let mutable maxScale = 4. let mutable minScale = 0.25 let currentImageScaleTransform = ScaleTransform() diff --git a/Parasitemia/Parasitemia/GUI/MainWindow.xaml b/Parasitemia/Parasitemia/GUI/MainWindow.xaml index 281493b..9aecf1b 100644 --- a/Parasitemia/Parasitemia/GUI/MainWindow.xaml +++ b/Parasitemia/Parasitemia/GUI/MainWindow.xaml @@ -1,5 +1,9 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + x:Name="MainWindow" Height="681.888" Width="787.61" MinHeight="200" MinWidth="300" Title="Parasitemia" Icon="pack://application:,,,/Resources/logo_256.png"> @@ -13,7 +17,6 @@ - @@ -32,7 +35,7 @@ - diff --git a/Parasitemia/Parasitemia/GUI/PiaZ.fs b/Parasitemia/Parasitemia/GUI/PiaZ.fs index 900ec65..a373e53 100644 --- a/Parasitemia/Parasitemia/GUI/PiaZ.fs +++ b/Parasitemia/Parasitemia/GUI/PiaZ.fs @@ -71,7 +71,7 @@ let save (filePath: string) (data: FileData) = rbc.num, rbc.infected, rbc.setManually, decimal rbc.center.X, decimal rbc.center.Y, decimal rbc.size.Width, decimal rbc.size.Height, - rbc.stainArea) |]) + rbc.infectedArea) |]) let imgJSONEntry = file.CreateEntry(imgFilename + ".json", CompressionLevel.Fastest) use imgJSONFileWriter = new StreamWriter(imgJSONEntry.Open()) @@ -98,5 +98,5 @@ let load (filePath: string) : FileData = { num = rbc.Num; infected = rbc.Infected; setManually = rbc.SetManually; center = Point(float rbc.PosX, float rbc.PosY); size = Size(float rbc.Width, float rbc.Height); - stainArea = rbc.StainArea } ] } ] + infectedArea = rbc.StainArea } ] } ] { sources = sources; patientID = mainJSON.PatientId } \ No newline at end of file diff --git a/Parasitemia/Parasitemia/GUI/RBCFrame.xaml b/Parasitemia/Parasitemia/GUI/RBCFrame.xaml new file mode 100644 index 0000000..2a56b00 --- /dev/null +++ b/Parasitemia/Parasitemia/GUI/RBCFrame.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Parasitemia/Parasitemia/GUI/RBCFrame.xaml.fs b/Parasitemia/Parasitemia/GUI/RBCFrame.xaml.fs new file mode 100644 index 0000000..1ef4d31 --- /dev/null +++ b/Parasitemia/Parasitemia/GUI/RBCFrame.xaml.fs @@ -0,0 +1,11 @@ +namespace Parasitemia.GUI.Views + +open System +open System.Windows +open System.Windows.Data +open System.Windows.Input + +open FSharp.ViewModule +open FsXaml + +type RBCFrame = XAML<"GUI/RBCFrame.xaml", true> diff --git a/Parasitemia/Parasitemia/GUI/State.fs b/Parasitemia/Parasitemia/GUI/State.fs index a1ae052..44de2e3 100644 --- a/Parasitemia/Parasitemia/GUI/State.fs +++ b/Parasitemia/Parasitemia/GUI/State.fs @@ -15,6 +15,16 @@ type State () = member val FilePath: string = "" with get, set member val PatientID: string = "" with get, set + member this.ImageParasitemia (srcImg: SourceImage) : int * int = + List.length srcImg.rbcs, + srcImg.rbcs |> List.fold (fun nbInfected rbc -> if rbc.infected then nbInfected + 1 else nbInfected) 0 + + member this.GlobalParasitemia : int * int = + sourceImages + |> Seq.fold (fun (nbTotal, nbTotalInfected) srcImg -> + let nb, nbInfected = this.ImageParasitemia srcImg + nbTotal + nb, nbTotalInfected + nbInfected) (0, 0) + member this.Save () = let data = { PiaZ.sources = List.ofSeq sourceImages; PiaZ.patientID = this.PatientID } PiaZ.save this.FilePath data @@ -24,23 +34,30 @@ type State () = this.PatientID <- data.patientID sourceImages.Clear() sourceImages.InsertRange(0, data.sources) + if sourceImages.Count > 0 + then this.CurrentImage <- Some sourceImages.[0] member this.AddSourceImage (filePath: string) : SourceImage = let srcImg = { num = sourceImages.Count + 1; rbcs = []; img = new Image(filePath) } sourceImages.Add(srcImg) + if sourceImages.Count = 1 + then this.CurrentImage <- Some sourceImages.[0] srcImg - member this.SetResult (num: int) (cells: Cell list) = - let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = num) + member this.SetResult (imgNum: int) (cells: Cell list) = + let sourceImage = sourceImages.Find(fun srcImg -> srcImg.num = imgNum) + let w = sourceImage.img.Width + let h = sourceImage.img.Height sourceImage.rbcs <- cells |> List.filter (fun cell -> match cell.cellClass with HealthyRBC | InfectedRBC -> true | _ -> false ) + |> List.sortByDescending (fun cell -> cell.infectedArea, (w - cell.center.X) + (h - cell.center.Y)) |> List.mapi (fun i cell -> - { num = i + { num = i + 1 infected = cell.cellClass = InfectedRBC setManually = false center = Point(float cell.center.X, float cell.center.Y) size = Size(float cell.elements.Width, float cell.elements.Height) - stainArea = cell.stainArea }) + infectedArea = cell.infectedArea }) member this.SourceImages : SourceImage seq = sourceImages :> SourceImage seq diff --git a/Parasitemia/Parasitemia/GUI/Types.fs b/Parasitemia/Parasitemia/GUI/Types.fs index e912ad3..6e00586 100644 --- a/Parasitemia/Parasitemia/GUI/Types.fs +++ b/Parasitemia/Parasitemia/GUI/Types.fs @@ -8,12 +8,17 @@ open Emgu.CV.Structure type RBC = { num: int - infected: bool - setManually: bool + mutable infected: bool + mutable setManually: bool center: Point size: Size - stainArea: int } + infectedArea: int } with + member this.SetAsInfected (infected: bool) = + if infected <> this.infected + then + this.infected <- infected + this.setManually <- not this.setManually type SourceImage = { num: int diff --git a/Parasitemia/Parasitemia/Parasitemia.fsproj b/Parasitemia/Parasitemia/Parasitemia.fsproj index d3289c6..b3a7e59 100644 --- a/Parasitemia/Parasitemia/Parasitemia.fsproj +++ b/Parasitemia/Parasitemia/Parasitemia.fsproj @@ -94,6 +94,8 @@ + + @@ -103,6 +105,7 @@ + diff --git a/Parasitemia/Parasitemia/Resources/logo_256.png b/Parasitemia/Parasitemia/Resources/logo_256.png new file mode 100644 index 0000000..857a793 Binary files /dev/null and b/Parasitemia/Parasitemia/Resources/logo_256.png differ diff --git a/Parasitemia/Parasitemia/Types.fs b/Parasitemia/Parasitemia/Types.fs index 872844b..5275b3a 100644 --- a/Parasitemia/Parasitemia/Types.fs +++ b/Parasitemia/Parasitemia/Types.fs @@ -48,6 +48,7 @@ type CellClass = HealthyRBC | InfectedRBC | Peculiar type Cell = { cellClass: CellClass center: Point + infectedArea: int stainArea: int elements: Matrix }