From db49e167a602ef1df02a8b5f7de334355a4917dd Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Mon, 25 Jan 2016 01:11:13 +0100 Subject: [PATCH] Add a way to detect the membrane of a parasite in the ring stage. --- Parasitemia/ParasitemiaCore/Classifier.fs | 49 ++++++++++++++----- Parasitemia/ParasitemiaCore/Config.fs | 36 ++++++++++---- Parasitemia/ParasitemiaCore/ImgTools.fs | 5 +- Parasitemia/ParasitemiaCore/MainAnalysis.fs | 45 +++++++++-------- .../ParasitemiaCore/ParasitesMarker.fs | 44 ++++++++++++----- Parasitemia/ParasitemiaUI/GUI.fs | 2 +- Parasitemia/ParasitemiaUI/PiaZ.fs | 9 +++- Parasitemia/ParasitemiaUI/State.fs | 4 +- 8 files changed, 132 insertions(+), 62 deletions(-) diff --git a/Parasitemia/ParasitemiaCore/Classifier.fs b/Parasitemia/ParasitemiaCore/Classifier.fs index 9f4fdd0..a3bfa9c 100644 --- a/Parasitemia/ParasitemiaCore/Classifier.fs +++ b/Parasitemia/ParasitemiaCore/Classifier.fs @@ -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() - let mutable stainPixels = 0 + let stainPixels = List() + + //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 }) diff --git a/Parasitemia/ParasitemiaCore/Config.fs b/Parasitemia/ParasitemiaCore/Config.fs index b12f9a7..9df9dc4 100644 --- a/Parasitemia/ParasitemiaCore/Config.fs +++ b/Parasitemia/ParasitemiaCore/Config.fs @@ -13,6 +13,9 @@ type Parameters = { rbcDiameter: float<μm> resolution: float + 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 // 220.e3 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. + LPFStandardDeviationParasite = 0.15<μm> + LPFStandardDeviationStain = 0.15<μm> // 0.12 + LPFStandardDeviationRBC = 0.2<μm> // 8.5e-6. // 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 = (μmToInch parameters.LPFStandardDeviation) * parameters.resolution + member this.LPFStandardDeviationParasite = + let stdDeviation: float = (μmToInch parameters.LPFStandardDeviationParasite) * parameters.resolution + float stdDeviation + + member this.LPFStandardDeviationStain = + let stdDeviation: float = (μmToInch parameters.LPFStandardDeviationStain) * parameters.resolution + float stdDeviation + + member this.LPFStandardDeviationRBC = + let stdDeviation: float = (μmToInch parameters.LPFStandardDeviationRBC) * parameters.resolution float stdDeviation member this.RBCRadiusByResolution = rbcRadiusByResolution diff --git a/Parasitemia/ParasitemiaCore/ImgTools.fs b/Parasitemia/ParasitemiaCore/ImgTools.fs index 3fb8ecf..4be893f 100644 --- a/Parasitemia/ParasitemiaCore/ImgTools.fs +++ b/Parasitemia/ParasitemiaCore/ImgTools.fs @@ -720,12 +720,13 @@ let private areaOperationF (img: Image) (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 diff --git a/Parasitemia/ParasitemiaCore/MainAnalysis.fs b/Parasitemia/ParasitemiaCore/MainAnalysis.fs index 4ab52d9..813d090 100644 --- a/Parasitemia/ParasitemiaCore/MainAnalysis.fs +++ b/Parasitemia/ParasitemiaCore/MainAnalysis.fs @@ -50,20 +50,28 @@ let doAnalysis (img: Image) (name: string) (config: Config) (reportPr logWithName "Starting analysis ..." - use green = img.[1] - let greenFloat = green.Convert() - let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation + use img_RBC = + use imgFloat = img.Convert() + 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() + 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()) 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) (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) (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) (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 } diff --git a/Parasitemia/ParasitemiaCore/ParasitesMarker.fs b/Parasitemia/ParasitemiaCore/ParasitesMarker.fs index ffe0d14..f4d9f35 100644 --- a/Parasitemia/ParasitemiaCore/ParasitesMarker.fs +++ b/Parasitemia/ParasitemiaCore/ParasitesMarker.fs @@ -48,12 +48,15 @@ let findMa (green: Image) (filteredGreen: Image) ( // * '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) (config: Config.Config) : Result * Image = - use filteredGreenWithoutInfection = filteredGreen.Copy() +let find (img: Image) (config: Config.Config) : Result * Image * Image = + + 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) (config: Config.Config) : Result diff._ThresholdBinary(Gray(0.0), Gray(255.)) diff.Convert() - 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 diff --git a/Parasitemia/ParasitemiaUI/GUI.fs b/Parasitemia/ParasitemiaUI/GUI.fs index d749479..f67c613 100644 --- a/Parasitemia/ParasitemiaUI/GUI.fs +++ b/Parasitemia/ParasitemiaUI/GUI.fs @@ -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 diff --git a/Parasitemia/ParasitemiaUI/PiaZ.fs b/Parasitemia/ParasitemiaUI/PiaZ.fs index 92397be..bdd6d07 100644 --- a/Parasitemia/ParasitemiaUI/PiaZ.fs +++ b/Parasitemia/ParasitemiaUI/PiaZ.fs @@ -95,7 +95,7 @@ let updateDocumentData (fromVersion: int) (toVersion: int) (data: DocumentData) /// /// /// If the file cannot be read -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(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 diff --git a/Parasitemia/ParasitemiaUI/State.fs b/Parasitemia/ParasitemiaUI/State.fs index 96dfc24..e15dc9b 100644 --- a/Parasitemia/ParasitemiaUI/State.fs +++ b/Parasitemia/ParasitemiaUI/State.fs @@ -9,7 +9,7 @@ open Emgu.CV.Structure open Types -type State () = +type State (defaultConfig: ParasitemiaCore.Config.Config) = let sourceImages = List() let mutable alteredSinceLastSave = false let mutable patientID = "" @@ -77,7 +77,7 @@ type State () = /// /// If the file cannot be loaded 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) -- 2.43.0