Add a way to detect the membrane of a parasite in the ring stage.
authorGreg Burri <greg.burri@gmail.com>
Mon, 25 Jan 2016 00:11:13 +0000 (01:11 +0100)
committerGreg Burri <greg.burri@gmail.com>
Mon, 25 Jan 2016 00:11:13 +0000 (01:11 +0100)
Parasitemia/ParasitemiaCore/Classifier.fs
Parasitemia/ParasitemiaCore/Config.fs
Parasitemia/ParasitemiaCore/ImgTools.fs
Parasitemia/ParasitemiaCore/MainAnalysis.fs
Parasitemia/ParasitemiaCore/ParasitesMarker.fs
Parasitemia/ParasitemiaUI/GUI.fs
Parasitemia/ParasitemiaUI/PiaZ.fs
Parasitemia/ParasitemiaUI/State.fs

index 9f4fdd0..a3bfa9c 100644 (file)
@@ -59,9 +59,9 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img:
                     then
                         let p' = Utils.pointFromTwoLines d1 d2
                         let delta, delta' =
-                            let d = c.X - p.X
-                            // To avoid rounding.
-                            if abs d < 0.001f then c.Y - p.Y, c.Y - p'.Y else d, c.X - p'.X
+                            let dx1, dx2 = (c.X - p.X), (c.X - p'.X)
+                            // To avoid rounding issue.
+                            if abs dx1 < 0.01f || abs dx2 < 0.01f then c.Y - p.Y, c.Y - p'.Y else dx1, dx2
 
                         // Yield 'false' when the point is owned by another ellipse.
                         if case1
@@ -148,6 +148,12 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img:
                     e.Removed <- true
 
         // 5) Define pixels associated to each ellipse and create the cells.
+        let radiusParasiteRatio = 0.4f
+        let radiusParasite = config.RBCRadius.Pixel * 0.5f
+        let perimeterParasiteSquared = (2.f * radiusParasite) ** 2.f |> roundInt
+        let parasiteOccupation = 0.08f // 8 %
+        let minimumParasiteArea = Const.PI * radiusParasite ** 2.f * parasiteOccupation |> roundInt
+        //let minimumStainArea = roundInt <| config.RBCRadius.Area * 0.02f // 1.5 %
         ellipsesWithNeigbors
         |> List.choose (fun (e, neighbors) ->
             if e.Removed
@@ -157,7 +163,9 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img:
                 let minX, minY, maxX, maxY = ellipseWindow e
 
                 let infectedPixels = List<Point>()
-                let mutable stainPixels = 0
+                let stainPixels = List<Point>()
+
+                //let mutable stainPixels = 0
                 let mutable darkStainPixels = 0
                 let mutable nbElement = 0
 
@@ -170,34 +178,49 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img:
                             elements.[y-minY, x-minX] <- 1uy
                             nbElement <- nbElement + 1
 
-                            if infection.Data.[y, x, 0] > 0uy
+                            let infected = infection.Data.[y, x, 0] > 0uy
+                            let stain = parasites.stain.Data.[y, x, 0] > 0uy
+                            let darkStain = parasites.darkStain.Data.[y, x, 0] > 0uy
+
+                            if infected
                             then
                                 infectedPixels.Add(Point(x, y))
 
-                            if parasites.stain.Data.[y, x, 0] > 0uy
+                            if stain
                             then
-                                stainPixels <- stainPixels + 1
+                                stainPixels.Add(Point(x, y))
 
-                            if parasites.darkStain.Data.[y, x, 0] > 0uy
+                            if darkStain
                             then
                                 darkStainPixels <- darkStainPixels + 1
 
+                let mutable stainArea = 0
+                if infectedPixels.Count > 0
+                then
+                    for stainPixel in stainPixels do
+                        if infectedPixels.Exists(fun p -> pown (p.X - stainPixel.X) 2 + pown (p.Y - stainPixel.Y) 2 <= perimeterParasiteSquared)
+                        then
+                            stainArea <- stainArea + 1
+
+
                 let cellClass =
-                    if float darkStainPixels > config.Parameters.maxDarkStainRatio * (float nbElement) ||
-                       float stainPixels > config.Parameters.maxStainRatio * (float nbElement)
+                    if float darkStainPixels > config.Parameters.maxDarkStainRatio * (float nbElement)
+                       //|| float stainPixels > config.Parameters.maxStainRatio * (float nbElement)
                     then
                         Peculiar
-                    elif infectedPixels.Count >= 1
+
+                    elif infectedPixels.Count > 0 && stainArea >= minimumParasiteArea
                     then
                         let infectionToRemove = ImgTools.connectedComponents parasites.stain infectedPixels
                         for p in infectionToRemove do
                             infection.Data.[p.Y, p.X, 0] <- 0uy
                         InfectedRBC
+
                     else
                         HealthyRBC
 
                 Some { cellClass = cellClass
                        center = Point(roundInt e.Cx, roundInt e.Cy)
-                       infectedArea = infectedPixels.Count
-                       stainArea = stainPixels
+                       infectedArea = if cellClass = InfectedRBC then infectedPixels.Count else 0
+                       stainArea = stainArea
                        elements = elements })
index b12f9a7..9df9dc4 100644 (file)
@@ -13,6 +13,9 @@ type Parameters = {
     rbcDiameter: float<μm>
     resolution: float<ppi>
 
+    colorContribution_BG_RBC: float * float * float // (R, G, B).
+    colorContribution_RBC_parasite: float * float * float // (R, G, B).
+
     ratioAreaPaleCenter: float32 // The area of the second opening is 'ratioSecondAreaOpen' * mean RBC area. It's applied only if greater than 'initialAreaOpen'.
 
     granulometryRange: float32 // The radius will be seeked from radius - granulometryRange * radius to radius + granulometryRange * radius.
@@ -20,7 +23,9 @@ type Parameters = {
     minRbcRadius: float32 // Factor of the mean RBC radius.
     maxRbcRadius: float32 // Factor of the mean RBC radius.
 
-    LPFStandardDeviation: float<μm> // Sigma parameter of the gaussian to remove the high frequency noise.
+    LPFStandardDeviationParasite: float<μm> // Sigma parameter of the gaussian to remove the high frequency noise.
+    LPFStandardDeviationStain: float<μm>
+    LPFStandardDeviationRBC: float<μm>
 
     // Ellipse.
     factorNbPick: float // The number of computed ellipse per edge pixel.
@@ -44,25 +49,30 @@ let defaultParameters = {
     rbcDiameter = 8.<μm>
     resolution = 220.e3<ppi> // 220.e3<ppi> Correspond to 50X.
 
-    ratioAreaPaleCenter = 1.f / 3.f // The ratio between an RBC area and the area of the its pale center.
+    colorContribution_BG_RBC = 0.16, 0.44, 0.4
+    colorContribution_RBC_parasite = 0.54, 0.41, 0.05
+
+    ratioAreaPaleCenter = 2.f / 5.f // The ratio between an RBC area and the area of the its pale center.
 
     granulometryRange = 0.5f
 
     minRbcRadius = -0.3f
     maxRbcRadius = 0.3f
 
-    LPFStandardDeviation = 0.2<μm> // 8.5e-6<inch>.
+    LPFStandardDeviationParasite = 0.15<μm>
+    LPFStandardDeviationStain = 0.15<μm> // 0.12
+    LPFStandardDeviationRBC = 0.2<μm> // 8.5e-6<inch>. // 0.2<μm>
 
     factorNbPick = 1.0
 
     darkStainLevel = 0.25 // 0.3
     maxDarkStainRatio = 0.1 // 10 %
 
-    infectionArea = 0.012f // 1.2 %
-    infectionSensitivity = 0.9
+    infectionArea = 0.01f // 0.8 % // 0.012f
+    infectionSensitivity = 0.9 // 1) 0.93, 2) 0.94
 
-    stainArea = 0.08f // 8 %
-    stainSensitivity = 0.9
+    stainArea = 0.08f // 6 % // 0.08f
+    stainSensitivity = 0.96 //  1) 0.91, 2) 0.92
     maxStainRatio = 0.12 // 12 %
 
     standardDeviationMaxRatio = 0.5 // 0.5
@@ -106,8 +116,16 @@ type Config (param: Parameters) =
 
     member val Debug = DebugOff with get, set
 
-    member this.LPFStandardDeviation =
-        let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviation) * parameters.resolution
+    member this.LPFStandardDeviationParasite =
+        let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviationParasite) * parameters.resolution
+        float stdDeviation
+
+    member this.LPFStandardDeviationStain =
+        let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviationStain) * parameters.resolution
+        float stdDeviation
+
+    member this.LPFStandardDeviationRBC =
+        let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviationRBC) * parameters.resolution
         float stdDeviation
 
     member this.RBCRadiusByResolution = rbcRadiusByResolution
index 3fb8ecf..4be893f 100644 (file)
@@ -720,12 +720,13 @@ let private areaOperationF (img: Image<Gray, float32>) (areas: (int * 'a) list)
                 else
                     if not <| Object.ReferenceEquals(other, null)
                     then // We touching another island.
-                        if island.IsInfinite || other.IsInfinite || island.Surface + other.Surface >= area
+                        if island.IsInfinite || other.IsInfinite || island.Surface + other.Surface >= area || comparer.Compare(island.Level, other.Level) < 0
                         then
                             stop <- true
                         else // We can merge 'other' into 'surface'.
                             island.Surface <- island.Surface + other.Surface
-                            island.Level <- if comparer.Compare(island.Level, other.Level) > 0 then island.Level else other.Level
+                            island.Level <- other.Level
+                            // island.Level <- if comparer.Compare(island.Level, other.Level) > 0 then other.Level else island.Level
                             for l, p in other.Shore do
                                 let mutable currentY = p.Y + 1
                                 while currentY < h && ownership.[currentY, p.X] = other do
index 4ab52d9..813d090 100644 (file)
@@ -50,20 +50,28 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
 
         logWithName "Starting analysis ..."
 
-        use green = img.[1]
-        let greenFloat = green.Convert<Gray, float32>()
-        let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation
+        use img_RBC =
+            use imgFloat = img.Convert<Bgr, float32>()
+            let redFactor, greenFactor, blueFactor = config.Parameters.colorContribution_BG_RBC
+            blueFactor * imgFloat.[0] + greenFactor * imgFloat.[1] + redFactor * imgFloat.[2]
+
+        let img_RBC_filtered = gaussianFilter img_RBC config.LPFStandardDeviationRBC
+
+        use img_parasites =
+            use imgFloat = img.Convert<Bgr, float32>()
+            let redFactor, greenFactor, blueFactor = config.Parameters.colorContribution_RBC_parasite
+            blueFactor * imgFloat.[0] + greenFactor * imgFloat.[1] + redFactor * imgFloat.[2]
 
         logWithName (sprintf "Nominal erytrocyte diameter: %A" config.RBCRadiusByResolution)
 
-        let initialAreaOpening = int <| config.RBCRadiusByResolution.Area * config.Parameters.ratioAreaPaleCenter * 1.2f // 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 () -> ImgTools.areaOpenF filteredGreen initialAreaOpening; report 10)
+        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 () -> ImgTools.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 r1 = log "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) range 1.0 |> float32)
-        let! radius = logTimeWithName "Granulometry (area)" (fun() -> reportWithVal 10 (Granulometry.findRadiusByAreaClosing filteredGreen range |> float32))
+        let! radius = logTimeWithName "Granulometry (area)" (fun() -> reportWithVal 10 (Granulometry.findRadiusByAreaClosing img_RBC_filtered range |> float32))
         config.SetRBCRadius <| radius
 
         logWithName (sprintf "Found erytrocyte diameter: %A" config.RBCRadius)
@@ -74,15 +82,17 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
             let secondAreaOpening = int <| config.RBCRadius.Area * config.Parameters.ratioAreaPaleCenter
             if secondAreaOpening > initialAreaOpening
             then
-                logTimeWithName "Second area opening" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening; report 30)
+                logTimeWithName "Second area opening" (fun () -> ImgTools.areaOpenF img_RBC_filtered secondAreaOpening; report 30)
             else
                 report 30
 
-        let parasites, filteredGreenWhitoutStain = ParasitesMarker.find filteredGreen config
+        ImgTools.areaCloseF img_RBC_filtered (config.RBCRadius.Area * 0.1f |> roundInt)
+
+        let parasites, filteredGreenWhitoutStain, filteredGreenWithoutInfection = ParasitesMarker.find img_parasites config
         //let parasites, filteredGreenWhitoutInfection, filteredGreenWhitoutStain = ParasitesMarker.findMa greenFloat filteredGreenFloat config
 
         let! edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () ->
-            let edges, xGradient, yGradient = ImgTools.findEdges filteredGreenWhitoutStain
+            let edges, xGradient, yGradient = ImgTools.findEdges img_RBC_filtered
             removeArea edges (config.RBCRadius.Pixel ** 2.f / 50.f |> int)
             reportWithVal 40 (edges, xGradient, yGradient))
 
@@ -90,7 +100,7 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
 
         let! prunedEllipses = logTimeWithName "Ellipses pruning" (fun () -> reportWithVal 80 (matchingEllipses.PrunedEllipses))
 
-        let! cells = logTimeWithName "Classifier" (fun () -> reportWithVal 100 (Classifier.findCells prunedEllipses parasites filteredGreenWhitoutStain config))
+        let! cells = logTimeWithName "Classifier" (fun () -> reportWithVal 100 (Classifier.findCells prunedEllipses parasites img_RBC_filtered config))
 
         logWithName "Analysis finished"
 
@@ -125,22 +135,17 @@ let doAnalysis (img: Image<Bgr, byte>) (name: string) (config: Config) (reportPr
                 drawCells imgCells' true cells
                 saveImg imgCells' (buildFileName " - cells - full.png")
 
-                let filteredGreenMaxima = gaussianFilter greenFloat config.LPFStandardDeviation
+                let filteredGreenMaxima = gaussianFilter img_RBC config.LPFStandardDeviationRBC
                 for m in ImgTools.findMaxima filteredGreenMaxima do
                     ImgTools.drawPoints filteredGreenMaxima m 255.f
                 saveImg filteredGreenMaxima (buildFileName " - filtered - maxima.png")
 
-                saveImg filteredGreen (buildFileName " - filtered.png")
+                saveImg img_RBC_filtered (buildFileName " - filtered.png")
                 saveImg filteredGreenWhitoutStain (buildFileName " - filtered closed stain.png")
-                //saveImg filteredGreenWhitoutInfection (buildFileName " - filtered closed infection.png")
-
-                saveImg green (buildFileName " - green.png")
-
-                use blue = img.Item(0)
-                saveImg blue (buildFileName " - blue.png")
+                saveImg filteredGreenWithoutInfection (buildFileName " - filtered closed infection.png")
 
-                use red = img.Item(2)
-                saveImg red (buildFileName " - red.png")
+                saveImg img_RBC (buildFileName " - source - RBC.png")
+                saveImg img_parasites (buildFileName " - source - parasites.png")
             | _ -> ()
 
         return cells }
index ffe0d14..f4d9f35 100644 (file)
@@ -48,12 +48,15 @@ let findMa (green: Image<Gray, float32>) (filteredGreen: Image<Gray, float32>) (
 // * 'Dark stain' corresponds to the colored pixel, it's independent of the size of the areas.
 // * 'Stain' corresponds to the stain around the parasites.
 // * 'Infection' corresponds to the parasite. It shouldn't contain thrombocytes.
-let find (filteredGreen: Image<Gray, float32>) (config: Config.Config) : Result * Image<Gray, float32> =
-    use filteredGreenWithoutInfection = filteredGreen.Copy()
+let find (img: Image<Gray, float32>) (config: Config.Config) : Result * Image<Gray, float32> * Image<Gray, float32> =
+
+    let imgFilteredInfection = ImgTools.gaussianFilter img config.LPFStandardDeviationParasite
+    let filteredGreenWithoutInfection = imgFilteredInfection.Copy()
     ImgTools.areaCloseF filteredGreenWithoutInfection (int config.RBCRadius.InfectionArea)
 
+    (*
     let filteredGreenWithoutStain = filteredGreenWithoutInfection.Copy()
-    ImgTools.areaCloseF filteredGreenWithoutStain (int config.RBCRadius.StainArea)
+    ImgTools.areaCloseF filteredGreenWithoutStain (int config.RBCRadius.StainArea) *)
 
     let darkStain =
         // We use the filtered image to find the dark stain.
@@ -69,19 +72,34 @@ let find (filteredGreen: Image<Gray, float32>) (config: Config.Config) : Result
         diff._ThresholdBinary(Gray(0.0), Gray(255.))
         diff.Convert<Gray, byte>()
 
-    let infectionMarker = marker filteredGreen filteredGreenWithoutInfection (1. / config.Parameters.infectionSensitivity)
-    let stainMarker = marker filteredGreenWithoutInfection filteredGreenWithoutStain (1. / config.Parameters.stainSensitivity)
-
-    // TODO: comprendre pourquoi des valeurs sont negatives!?!?
-    (*
-    let blackTopHat = filteredGreen.CopyBlank()
-    CvInvoke.Subtract(filteredGreenWithoutInfection, filteredGreen, blackTopHat)
-    ImgTools.saveImg (ImgTools.normalizeAndConvert blackTopHat) "BottomHat.png"
-    *)
+    let infectionMarker = marker imgFilteredInfection filteredGreenWithoutInfection (1. / config.Parameters.infectionSensitivity)
+
+    let imgFilteredStain = ImgTools.gaussianFilter img config.LPFStandardDeviationStain
+    let areaOpening = int <| config.RBCRadius.Area * config.Parameters.ratioAreaPaleCenter
+    //ImgTools.areaOpenF imgFilteredStain areaOpening
+
+    let filteredGreenWithoutStain = imgFilteredStain.CopyBlank()
+    let kernelSize =
+        let size = roundInt (config.RBCRadius.Pixel / 5.f)
+        if size % 2 = 0 then size + 1 else size
+    use kernel =
+        if kernelSize <= 3
+        then
+            CvInvoke.GetStructuringElement(CvEnum.ElementShape.Rectangle, Size(3, 3), Point(-1, -1))
+        else
+            CvInvoke.GetStructuringElement(CvEnum.ElementShape.Ellipse, Size(kernelSize, kernelSize), Point(-1, -1))
+    CvInvoke.MorphologyEx(imgFilteredStain, filteredGreenWithoutStain, CvEnum.MorphOp.Close, kernel, Point(-1, -1), 1, CvEnum.BorderType.Replicate, MCvScalar())
+    let stainMarker = marker (*filteredGreenWithoutInfection*) imgFilteredStain filteredGreenWithoutStain (1. / config.Parameters.stainSensitivity)
+
+    //
+    (*let blackTopHat = filteredGreenWithoutStain.CopyBlank()
+    CvInvoke.Subtract(filteredGreenWithoutStain, imgFilteredStain, blackTopHat)
+    ImgTools.saveImg blackTopHat "blackTopHat.png"*)
 
     { darkStain = darkStain
       infection = infectionMarker
       stain = stainMarker },
-    filteredGreenWithoutStain
+    filteredGreenWithoutStain,
+    filteredGreenWithoutInfection
 
 
index d749479..f67c613 100644 (file)
@@ -24,7 +24,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let mainWindow = Views.MainWindow()
     let ctrl (name: string): 'a = mainWindow.Root.FindName(name) :?> 'a
 
-    let state = State.State()
+    let state = State.State(defaultConfig)
     let mutable currentScale = 1.
     let mutable displayHealthy = false
     let warningBelowNumberOfRBC = 1000
index 92397be..bdd6d07 100644 (file)
@@ -95,7 +95,7 @@ let updateDocumentData (fromVersion: int) (toVersion: int) (data: DocumentData)
 /// </summary>
 /// <param name="filePath"></param>
 /// <exception cref="System.IOException">If the file cannot be read</exception>
-let load (filePath: string) : DocumentData =
+let load (filePath: string) (defaultConfig: ParasitemiaCore.Config.Config) : DocumentData =
     use file = ZipFile.Open(filePath, ZipArchiveMode.Read)
 
     let mainEntry = file.GetEntry(mainEntryName)
@@ -114,7 +114,12 @@ let load (filePath: string) : DocumentData =
                             let imgJSONEntry = file.GetEntry(imgEntry.Name + ".json")
                             use imgJSONFileReader = new StreamReader(imgJSONEntry.Open())
                             let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage>(imgJSONFileReader.ReadToEnd())
-                            let config = ParasitemiaCore.Config.Config(imgInfo.parameters)
+
+                            let config = defaultConfig.Copy()
+                            config.Parameters <-
+                                { ParasitemiaCore.Config.defaultParameters with
+                                    resolution = imgInfo.parameters.resolution }
+
                             config.SetRBCRadius imgInfo.RBCRadius
                             yield { num = imgNum
                                     name = imgInfo.name
index 96dfc24..e15dc9b 100644 (file)
@@ -9,7 +9,7 @@ open Emgu.CV.Structure
 
 open Types
 
-type State () =
+type State (defaultConfig: ParasitemiaCore.Config.Config) =
     let sourceImages = List<SourceImage>()
     let mutable alteredSinceLastSave = false
     let mutable patientID = ""
@@ -77,7 +77,7 @@ type State () =
     /// </summary>
     /// <exception cref="System.IOException">If the file cannot be loaded</exception>
     member this.Load () =
-        let data = PiaZ.load this.FilePath
+        let data = PiaZ.load this.FilePath defaultConfig
         this.PatientID <- data.patientID
         sourceImages.Clear()
         sourceImages.InsertRange(0, data.images)