From 05be8164d308447b916544ae3ce4211500dfd8da Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Fri, 15 Jan 2016 10:13:54 +0100 Subject: [PATCH] Use two radius in the configuration, one computed with the image resolution and one by granulometry. --- Parasitemia/Parasitemia/Classifier.fs | 44 ++-------------- Parasitemia/Parasitemia/Config.fs | 60 +++++++++++++--------- Parasitemia/Parasitemia/Ellipse.fs | 2 +- Parasitemia/Parasitemia/GUI/Analysis.fs | 9 +--- Parasitemia/Parasitemia/GUI/GUI.fs | 2 +- Parasitemia/Parasitemia/GUI/State.fs | 2 +- Parasitemia/Parasitemia/MainAnalysis.fs | 19 ++++--- Parasitemia/Parasitemia/Parasitemia.fsproj | 3 +- Parasitemia/Parasitemia/ParasitesMarker.fs | 4 +- 9 files changed, 59 insertions(+), 86 deletions(-) diff --git a/Parasitemia/Parasitemia/Classifier.fs b/Parasitemia/Parasitemia/Classifier.fs index 1a22129..121a7f5 100644 --- a/Parasitemia/Parasitemia/Classifier.fs +++ b/Parasitemia/Parasitemia/Classifier.fs @@ -29,10 +29,10 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img: let infection = parasites.infection.Copy() // To avoid to modify the parameter. // This is the minimum window size to check if other ellipses touch 'e'. - let searchRegion (e: Ellipse) = { KdTree.minX = e.Cx - (e.A + config.RBCMaxRadius) - KdTree.maxX = e.Cx + (e.A + config.RBCMaxRadius) - KdTree.minY = e.Cy - (e.A + config.RBCMaxRadius) - KdTree.maxY = e.Cy + (e.A + config.RBCMaxRadius) } + let searchRegion (e: Ellipse) = { KdTree.minX = e.Cx - (e.A + config.RBCRadius.Max) + KdTree.maxX = e.Cx + (e.A + config.RBCRadius.Max) + KdTree.minY = e.Cy - (e.A + config.RBCRadius.Max) + KdTree.maxY = e.Cy + (e.A + config.RBCRadius.Max) } // The minimum window to contain a given ellipse. let ellipseWindow (e: Ellipse) = @@ -97,7 +97,6 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img: // We reverse the list to get the lower score ellipses first. let ellipsesWithNeigbors = ellipses |> List.map (fun e -> e, neighbors e) |> List.rev - // 2) Remove ellipses touching the edges. for e in ellipses do if e.isOutside w_f h_f then e.Removed <- true @@ -125,42 +124,9 @@ let findCells (ellipses: Ellipse list) (parasites: ParasitesMarker.Result) (img: if stdDeviation > globalStdDeviation * config.Parameters.standardDeviationMaxRatio then e.Removed <- true -(* - let imgData = img.Data - let stdDeviations = [ - for e in ellipses do - if not e.Removed - then - let shrinkedE = e.Scale 0.9f - let minX, minY, maxX, maxY = ellipseWindow shrinkedE - - let stdDeviation = float32 <| MathNet.Numerics.Statistics.Statistics.StandardDeviation (seq { - for y in (if minY < 0 then 0 else minY) .. (if maxY >= h then h - 1 else maxY) do - for x in (if minX < 0 then 0 else minX) .. (if maxX >= w then w - 1 else maxX) do - if shrinkedE.Contains (float32 x) (float32 y) - then - yield float imgData.[y, x, 0] }) - - e.StdDeviation <- stdDeviation - yield stdDeviation ] - - // We use Otsu and eliminate some cells only if the curve may be bimodal. - // See https://en.wikipedia.org/wiki/Multimodal_distribution#Bimodality_coefficient - let skewness, kurtosis = MathNet.Numerics.Statistics.Statistics.PopulationSkewnessKurtosis (stdDeviations |> List.map float) - let n = float stdDeviations.Length - let bimodalityCoefficient = (skewness ** 2. + 1.) / (kurtosis + 3. * (n - 1.) ** 2. / ((n - 2.) * (n - 3.))) - - if bimodalityCoefficient > 5. / 9. - then - let hist = ImgTools.histogram stdDeviations 200 - let thresh, _, _ = ImgTools.otsu hist - for e in ellipses do - if not e.Removed && e.StdDeviation > thresh - then e.Removed <- true -*) // 4) Remove ellipses with little area. - let minArea = config.RBCMinArea + let minArea = config.RBCRadius.MinArea for e, neighbors in ellipsesWithNeigbors do if not e.Removed then diff --git a/Parasitemia/Parasitemia/Config.fs b/Parasitemia/Parasitemia/Config.fs index e28398e..0916408 100644 --- a/Parasitemia/Parasitemia/Config.fs +++ b/Parasitemia/Parasitemia/Config.fs @@ -68,40 +68,54 @@ let defaultParameters = { standardDeviationMaxRatio = 0.5 // 0.5 minimumCellAreaFactor = 0.4f } -type Config (param: Parameters) = - let mutable parameters: Parameters = param +type RBCRadius (radius: float32, parameters: Parameters) = + member this.Pixel = radius + member this.μm : float<μm> = + 1. * (float radius) / parameters.resolution |> inchToμm + + member this.Min = radius + parameters.minRbcRadius * radius + member this.Max = radius + parameters.maxRbcRadius * radius + + member this.Area = PI * radius ** 2.f + member this.MinArea = parameters.minimumCellAreaFactor * radius - let initialRBCRadius : float32 = - let rbcRadiusInch: float = (μmToInch parameters.rbcDiameter) / 2. - let rbcRadiusPx: float = parameters.resolution * rbcRadiusInch + member this.InfectionArea = parameters.infectionArea * this.Area + member this.StainArea = parameters.stainArea * this.Area + + override this.ToString() = + sprintf "%d px (%.1f μm)" (Utils.roundInt <| 2.f * radius) (2. * this.μm) + + +type Config (param: Parameters) = + let RBCadiusInPixels (rbcDiameter: float<μm>) (resolution: float) : float32 = + let rbcRadiusInch: float = (μmToInch rbcDiameter) / 2. + let rbcRadiusPx: float = resolution * rbcRadiusInch float32 rbcRadiusPx + let mutable parameters: Parameters = param + let mutable rbcRadiusByResolution = RBCRadius(RBCadiusInPixels parameters.rbcDiameter parameters.resolution, parameters) + let mutable rbcRadius = RBCRadius(0.f, parameters) + new () = Config(defaultParameters) - member this.Parameters with get() = parameters and set(param) = parameters <- param + member this.Parameters + with get() = parameters + and set(param) = + parameters <- param + rbcRadiusByResolution <- RBCRadius(RBCadiusInPixels parameters.rbcDiameter parameters.resolution, param) + rbcRadius <- RBCRadius(rbcRadius.Pixel, param) + member val Debug = DebugOff with get, set member this.LPFStandardDeviation = - let stdDeviation: float = (μmToInch parameters.LPFStandardDeviation) * param.resolution + let stdDeviation: float = (μmToInch parameters.LPFStandardDeviation) * parameters.resolution float stdDeviation - // Mean RBC radius. - member val RBCRadius : float32 = initialRBCRadius with get, set - - member this.RBCRadiusμm : float<μm> = - 1. * (float this.RBCRadius) / parameters.resolution |> inchToμm - - member this.RBCMinRadius = this.RBCRadius + parameters.minRbcRadius * this.RBCRadius - member this.RBCMaxRadius = this.RBCRadius + parameters.maxRbcRadius * this.RBCRadius - - member this.RBCArea = PI * this.RBCRadius ** 2.f - member this.RBCMinArea = parameters.minimumCellAreaFactor * this.RBCArea - - member this.InfectionArea = parameters.infectionArea * this.RBCArea - member this.StainArea = parameters.stainArea * this.RBCArea + member this.RBCRadiusByResolution = rbcRadiusByResolution + member this.RBCRadius = rbcRadius - member this.FormattedRadius = - sprintf "%d px (%.1f μm)" (Utils.roundInt <| 2.f * this.RBCRadius) (2. * this.RBCRadiusμm) + member this.SetRBCRadius (radiusPixel: float32) = + rbcRadius <- RBCRadius(radiusPixel, parameters) member this.Copy () = this.MemberwiseClone() :?> Config diff --git a/Parasitemia/Parasitemia/Ellipse.fs b/Parasitemia/Parasitemia/Ellipse.fs index c8d44b7..be35940 100644 --- a/Parasitemia/Parasitemia/Ellipse.fs +++ b/Parasitemia/Parasitemia/Ellipse.fs @@ -209,7 +209,7 @@ let find (edges: Matrix) (yGradient: Image) (config: Config) : MatchingEllipses = - let r1, r2 = config.RBCMinRadius, config.RBCMaxRadius + let r1, r2 = config.RBCRadius.Min, config.RBCRadius.Max let incrementWindowDivisor = 4.f // We choose a window size for which the biggest ellipse can always be fitted in. diff --git a/Parasitemia/Parasitemia/GUI/Analysis.fs b/Parasitemia/Parasitemia/GUI/Analysis.fs index ccca1b4..6ae1e20 100644 --- a/Parasitemia/Parasitemia/GUI/Analysis.fs +++ b/Parasitemia/Parasitemia/GUI/Analysis.fs @@ -121,7 +121,7 @@ let showWindow (parent: Window) (state: State.State) : bool = butClose.Content <- "Close" updateSourceImages ()) - Utils.log "Analysis terminated successfully" + Utils.log "All analyses terminated successfully" atLeastOneAnalysisPerformed <- true analysisPerformed <- true) } |> Async.Start) @@ -134,9 +134,4 @@ let showWindow (parent: Window) (state: State.State) : bool = if not analysisPerformed then analysisCancelled <- true - atLeastOneAnalysisPerformed) - - - (*let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig - for id, rbcRadius, cells in results do - state.SetResult (int id) rbcRadius cells*) \ No newline at end of file + atLeastOneAnalysisPerformed) \ No newline at end of file diff --git a/Parasitemia/Parasitemia/GUI/GUI.fs b/Parasitemia/Parasitemia/GUI/GUI.fs index b0af4cc..65c3cdc 100644 --- a/Parasitemia/Parasitemia/GUI/GUI.fs +++ b/Parasitemia/Parasitemia/GUI/GUI.fs @@ -114,7 +114,7 @@ let run (defaultConfig: Config) = txtImageInformation.Inlines.Add(Documents.LineBreak()) txtImageInformation.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold)) - txtImageInformation.Inlines.Add(Documents.Run(srcImg.config.FormattedRadius)) + txtImageInformation.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString())) txtImageInformation.Inlines.Add(Documents.LineBreak()) txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold)) diff --git a/Parasitemia/Parasitemia/GUI/State.fs b/Parasitemia/Parasitemia/GUI/State.fs index 3b9d11e..45272ca 100644 --- a/Parasitemia/Parasitemia/GUI/State.fs +++ b/Parasitemia/Parasitemia/GUI/State.fs @@ -91,7 +91,7 @@ type State () = // To match with previously manually altered RBC. let manuallyAlteredPreviousRBCS = sourceImage.rbcs |> List.filter (fun rbc -> rbc.setManually) - let tolerance = (float sourceImage.config.RBCRadius) * 0.5 // +/-. + let tolerance = (float sourceImage.config.RBCRadius.Pixel) * 0.5 // +/-. let getPreviousRBC (center: Point) : RBC option = manuallyAlteredPreviousRBCS |> List.tryFind (fun rbc -> rbc.center.X > center.X - tolerance && rbc.center.X < center.X + tolerance && rbc.center.Y > center.Y - tolerance && rbc.center.Y < center.Y + tolerance) diff --git a/Parasitemia/Parasitemia/MainAnalysis.fs b/Parasitemia/Parasitemia/MainAnalysis.fs index a7fdf71..8f59514 100644 --- a/Parasitemia/Parasitemia/MainAnalysis.fs +++ b/Parasitemia/Parasitemia/MainAnalysis.fs @@ -31,26 +31,25 @@ let doAnalysis (img: Image) (name: string) (config: Config) (reportPr let greenFloat = green.Convert() let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation - logWithName (sprintf "Nominal erytrocyte diameter: %s" config.FormattedRadius) + logWithName (sprintf "Nominal erytrocyte diameter: %A" config.RBCRadiusByResolution) - let initialAreaOpening = int<| config.RBCArea * 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. + 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. logTimeWithName "Area opening number one" (fun () -> ImgTools.areaOpenF filteredGreen initialAreaOpening) report 8 let range = - let delta = config.Parameters.granulometryRange * config.RBCRadius - int <| config.RBCRadius - delta, int <| config.RBCRadius + delta + 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 r2 = logTimeWithName "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen range |> float32) + config.SetRBCRadius <| logTimeWithName "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen range |> float32) // log (sprintf "r1: %A, r2: %A" r1 r2) - config.RBCRadius <- r2 - logWithName (sprintf "Found erytrocyte diameter: %s" config.FormattedRadius) + logWithName (sprintf "Found erytrocyte diameter: %A" config.RBCRadius) report 24 - let secondAreaOpening = int <| config.RBCArea * config.Parameters.ratioAreaPaleCenter + let secondAreaOpening = int <| config.RBCRadius.Area * config.Parameters.ratioAreaPaleCenter if secondAreaOpening > initialAreaOpening then logTimeWithName "Area opening number two" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening) @@ -60,7 +59,7 @@ let doAnalysis (img: Image) (name: string) (config: Config) (reportPr let edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () -> let edges, xGradient, yGradient = ImgTools.findEdges filteredGreenWhitoutStain - removeArea edges (config.RBCRadius ** 2.f / 50.f |> int) + removeArea edges (config.RBCRadius.Pixel ** 2.f / 50.f |> int) edges, xGradient, yGradient) let allEllipses, ellipses = logTimeWithName "Finding ellipses" (fun () -> @@ -139,7 +138,7 @@ let doMultipleAnalysis (imgs: (string * Config * Image) list) (report progressPerAnalysis.AddOrUpdate(id, progress, (fun _ _ -> progress)) |> ignore report (progressPerAnalysis.Values.Sum() / nbImgs) - let nbConcurrentTaskLimit = 4 + let nbConcurrentTaskLimit = 4 // To reduce the memory taken. let n = Environment.ProcessorCount imgs diff --git a/Parasitemia/Parasitemia/Parasitemia.fsproj b/Parasitemia/Parasitemia/Parasitemia.fsproj index 81198db..ed05b74 100644 --- a/Parasitemia/Parasitemia/Parasitemia.fsproj +++ b/Parasitemia/Parasitemia/Parasitemia.fsproj @@ -40,8 +40,7 @@ x64 bin\Debug\Parasitemia.XML false - - + --output "../../../Images/output" --debug bin\DebugGUI\ diff --git a/Parasitemia/Parasitemia/ParasitesMarker.fs b/Parasitemia/Parasitemia/ParasitesMarker.fs index a40e463..a6fce83 100644 --- a/Parasitemia/Parasitemia/ParasitesMarker.fs +++ b/Parasitemia/Parasitemia/ParasitesMarker.fs @@ -51,10 +51,10 @@ let findMa (green: Image) (filteredGreen: Image) ( // * 'Infection' corresponds to the parasite. It shouldn't contain thrombocytes. let find (filteredGreen: Image) (config: Config.Config) : Result * Image = use filteredGreenWithoutInfection = filteredGreen.Copy() - ImgTools.areaCloseF filteredGreenWithoutInfection (int config.InfectionArea) + ImgTools.areaCloseF filteredGreenWithoutInfection (int config.RBCRadius.InfectionArea) let filteredGreenWithoutStain = filteredGreenWithoutInfection.Copy() - ImgTools.areaCloseF filteredGreenWithoutStain (int config.StainArea) + ImgTools.areaCloseF filteredGreenWithoutStain (int config.RBCRadius.StainArea) let darkStain = // We use the filtered image to find the dark stain. -- 2.43.0