maxDarkStainRatio: float // When a cell must own less than this ratio to be a RBC.
stainArea: float32 // Factor of a RBC area. 0.5 means the half of RBC area.
- stainLevel: float // > 1
- maxStainRatio: float // When a cell must own less than this ratio to be a RBC.
+ stainSensitivity: float // between 0 (the least sensitive) and 1 (the most sensitive).
+ maxStainRatio: float // A cell must own less than this ratio to be a RBC.
infectionArea: float32 // Factor of a RBC area. 0.5 means the half of RBC area.
- infectionLevel: float // > 1
+ infectionSensitivity: float // between 0 (the least sensitive) and 1 (the most sensitive).
standardDeviationMaxRatio: float // The standard deviation of the pixel values of a cell can't be greater than standardDeviationMaxRatio * global standard deviation
- minimumCellAreaFactor: float32 // Factor of the mean RBC area.
+ minimumCellAreaFactor: float32 // Factor of the mean RBC area. A cell with an area below this will be rejected.
}
let defaultParameters = {
maxDarkStainRatio = 0.1 // 10 %
infectionArea = 0.012f // 1.2 %
- infectionLevel = 1.12 // Lower -> more sensitive.
+ infectionSensitivity = 0.9
stainArea = 0.08f // 8 %
- stainLevel = 1.1 // Lower -> more sensitive.
+ stainSensitivity = 0.9
maxStainRatio = 0.12 // 12 %
standardDeviationMaxRatio = 0.5 // 0.5
minimumCellAreaFactor = 0.4f }
type Config (param: Parameters) =
+ let mutable parameters: Parameters = param
+
let initialRBCRadius : float32 =
- let rbcRadiusInch: float<inch> = μm.ConvertToInch(param.rbcDiameter) / 2.
- let rbcRadiusPx: float<px> = param.resolution * rbcRadiusInch
+ let rbcRadiusInch: float<inch> = (μmToInch parameters.rbcDiameter) / 2.
+ let rbcRadiusPx: float<px> = parameters.resolution * rbcRadiusInch
float32 rbcRadiusPx
- member this.Parameters = param
+ new () = Config(defaultParameters)
+
+ member this.Parameters with get() = parameters and set(param) = parameters <- param
member val Debug = DebugOff with get, set
member this.LPFStandardDeviation =
- let stdDeviation: float<px> = μm.ConvertToInch(param.LPFStandardDeviation) * param.resolution
+ let stdDeviation: float<px> = (μmToInch parameters.LPFStandardDeviation) * param.resolution
float stdDeviation
// Mean RBC radius.
member val RBCRadius : float32 = initialRBCRadius with get, set
- member this.RBCMinRadius = this.RBCRadius + param.minRbcRadius * this.RBCRadius
- member this.RBCMaxRadius = this.RBCRadius + param.maxRbcRadius * this.RBCRadius
+ member this.RBCRadiusμm : float<μm> =
+ 1.<px> * (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 = param.minimumCellAreaFactor * this.RBCArea
+ member this.RBCMinArea = parameters.minimumCellAreaFactor * this.RBCArea
+
+ member this.InfectionArea = parameters.infectionArea * this.RBCArea
+ member this.StainArea = parameters.stainArea * this.RBCArea
- member this.InfectionArea = param.infectionArea * this.RBCArea
- member this.StainArea = param.stainArea * this.RBCArea
+ member this.FormattedRadius =
+ sprintf "%d px (%.1f μm)" (Utils.roundInt <| 2.f * this.RBCRadius) (2. * this.RBCRadiusμm)
member this.Copy () =
this.MemberwiseClone() :?> Config
module Parasitemia.GUI.Analysis
open System
+open System.IO
+open System.Linq
open System.Windows
open System.Windows.Media
open System.Windows.Markup
open System.Windows.Shapes
open System.Windows.Controls
+open System.Diagnostics
+open Microsoft.Win32 // For the common dialogs.
+open Emgu.CV.WPF
+
+open UnitsOfMeasure
open Config
+open Types
-let showWindow (parent: Window) (state: State.State) (defaultConfig: Config) : bool =
+let showWindow (parent: Window) (state: State.State) : bool =
let window = Views.AnalysisWindow()
window.Root.Owner <- parent
window.Root.Left <- parent.Left + parent.ActualWidth / 2. - window.Root.Width / 2.
let butClose: Button = ctrl "butClose"
let butStart: Button = ctrl "butStart"
+ let stackImagesSourceSelection: StackPanel = ctrl "stackImagesSourceSelection"
let progressBar: ProgressBar = ctrl "progress"
let textLog: TextBlock = ctrl "textLog"
let scrollLog: ScrollViewer = ctrl "scrollLog"
textLog.Inlines.Add(Documents.LineBreak())
scrollLog.ScrollToBottom()))
+ let minPPI = 1.
+ let maxPPI = 10e6
+ let parseAndValidatePPI (input: string) : float option =
+ let res = ref 0.
+ if Double.TryParse(input, res) && !res >= minPPI && !res <= maxPPI
+ then Some !res
+ else None
+
let monitor = Object()
+ let mutable atLeastOneAnalysisPerformed = false
let mutable analysisPerformed = false
let mutable analysisCancelled = false
+ let updateSourceImages () =
+ stackImagesSourceSelection.Children.Clear()
+ let width = int stackImagesSourceSelection.ActualWidth
+ for srcImg in state.SourceImages do
+ let imageSourceSelection = Views.ImageSourceSelection(Tag = srcImg, Margin = Thickness(3.))
+
+ let updateResolution () =
+ match parseAndValidatePPI imageSourceSelection.txtResolution.Text with
+ | Some resolution -> srcImg.config.Parameters <- { srcImg.config.Parameters with resolution = resolution * 1.<ppi> }
+ | None -> ()
+
+ imageSourceSelection.txtImageNumber.Text <- srcImg.num.ToString()
+ let height = srcImg.img.Height * width / srcImg.img.Width
+ imageSourceSelection.imagePreview.Source <- BitmapSourceConvert.ToBitmapSource(srcImg.img.Resize(width, height, Emgu.CV.CvEnum.Inter.Cubic))
+ imageSourceSelection.chkSelection.IsChecked <- Nullable<bool>(srcImg.dateLastAnalysis.Ticks = 0L)
+ imageSourceSelection.lblDateLastAnalysis.Content <- if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToString()
+
+ imageSourceSelection.txtResolution.Text <- srcImg.config.Parameters.resolution.ToString()
+ imageSourceSelection.menuZoom50X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "200000"; updateResolution ())
+ imageSourceSelection.menuZoom100X.Click.AddHandler(fun obj args -> imageSourceSelection.txtResolution.Text <- "400000"; updateResolution ())
+
+ imageSourceSelection.txtResolution.PreviewTextInput.AddHandler(fun obj args ->
+ let text = imageSourceSelection.txtResolution.Text + args.Text
+ args.Handled <- match parseAndValidatePPI text with Some _ -> false | None -> true)
+
+ imageSourceSelection.imagePreview.MouseLeftButtonDown.AddHandler(fun obj args ->
+ let checkbox = imageSourceSelection.chkSelection
+ checkbox.IsChecked <- Nullable<bool>(not (checkbox.IsChecked.HasValue && checkbox.IsChecked.Value)))
+
+ imageSourceSelection.txtResolution.LostFocus.AddHandler(fun obj args -> updateResolution ())
+
+ stackImagesSourceSelection.Children.Add(imageSourceSelection) |> ignore
+
butClose.Click.AddHandler(fun obj args -> window.Root.Close())
butStart.Click.AddHandler(fun obj args ->
- butStart.IsEnabled <- false
- butClose.Content <- "Abort"
- async {
- let results =
- ImageAnalysis.doMultipleAnalysis
- (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList)
- defaultConfig
- (Some (fun progress -> window.Root.Dispatcher.Invoke(fun () -> progressBar.Value <- float progress)))
-
- lock monitor (
- fun() ->
- if not analysisCancelled
- then
- for id, rbcRadius, cells in results do
- state.SetResult (int id) rbcRadius cells
-
- window.Root.Dispatcher.Invoke(fun () ->
- butStart.IsEnabled <- false
- butClose.Content <- "Close")
-
- Utils.log "Analysis terminated successfully"
- analysisPerformed <- true)
- } |> Async.Start)
+ let imagesToProcess = [
+ for imageSelection in stackImagesSourceSelection.Children |> Seq.cast<Views.ImageSourceSelection> do
+ let chk = imageSelection.chkSelection.IsChecked
+ if chk.HasValue && chk.Value
+ then
+ let srcImg = imageSelection.Tag :?> SourceImage
+ yield srcImg.num.ToString(), srcImg.config, srcImg.img ]
+
+ if imagesToProcess.IsEmpty
+ then
+ MessageBox.Show("No image selected", "Cannot start analysis", MessageBoxButton.OK, MessageBoxImage.Information) |> ignore
+ else
+ analysisPerformed <- false
+ butStart.IsEnabled <- false
+ butClose.Content <- "Abort"
+ async {
+ let results =
+ ImageAnalysis.doMultipleAnalysis
+ imagesToProcess
+ (Some (fun progress -> window.Root.Dispatcher.Invoke(fun () -> progressBar.Value <- float progress)))
+
+ lock monitor (
+ fun() ->
+ if not analysisCancelled
+ then
+ for id, cells in results do
+ state.SetResult (int id) cells
+
+ window.Root.Dispatcher.Invoke(fun () ->
+ butStart.IsEnabled <- true
+ butClose.Content <- "Close"
+ updateSourceImages ())
+
+ Utils.log "Analysis terminated successfully"
+ atLeastOneAnalysisPerformed <- true
+ analysisPerformed <- true)
+ } |> Async.Start)
+
+ window.Root.Loaded.AddHandler(fun obj args -> updateSourceImages ())
window.Root.ShowDialog() |> ignore
if not analysisPerformed
then
analysisCancelled <- true
- analysisPerformed)
+ atLeastOneAnalysisPerformed)
(*let results = ImageAnalysis.doMultipleAnalysis (state.SourceImages |> Seq.map (fun srcImg -> string srcImg.num, srcImg.img) |> Seq.toList) defaultConfig
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
- x:Name="AnalysisWindow" Height="350" Width="500" MinHeight="100" MinWidth="100" Title="Analysis">
+ x:Name="AnalysisWindow" Height="453" Width="515" MinHeight="100" MinWidth="100" Title="Analysis" Icon="pack://application:,,,/Resources/logo_256.png">
<Grid>
<Grid.RowDefinitions>
- <RowDefinition Height="100"/>
+ <RowDefinition Height="50*"/>
<RowDefinition Height="30"/>
- <RowDefinition/>
+ <RowDefinition Height="20*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
- <ScrollViewer x:Name="scrollImagesSourceSelection" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Visible" Grid.Row="0" Margin="3" >
- <StackPanel x:Name="stackImagesSourceSelection" Orientation="Horizontal" />
+ <ScrollViewer x:Name="scrollImagesSourceSelection" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Grid.Row="0" Margin="3" >
+ <StackPanel x:Name="stackImagesSourceSelection" />
</ScrollViewer>
<ProgressBar x:Name="progress" Grid.Row="1" Margin="3" Minimum="0" Maximum="100" />
<ScrollViewer x:Name="scrollLog" Grid.Row="2" Margin="3" HorizontalScrollBarVisibility="Auto">
txtImageInformation.Inlines.Add(Documents.LineBreak())
txtImageInformation.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
- txtImageInformation.Inlines.Add(Documents.Run(string (Utils.roundInt <| 2. * srcImg.rbcRadius)))
- txtImageInformation.Inlines.Add(Documents.Run(" px"))
+ txtImageInformation.Inlines.Add(Documents.Run(srcImg.config.FormattedRadius))
txtImageInformation.Inlines.Add(Documents.LineBreak())
txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
then
imgCtrl.ReleaseMouseCapture())
-
let updatePreviews () =
stackPreviews.Children.Clear ()
for srcImg in state.SourceImages do
updatePreviews ()
updateGlobalParasitemia ()
- txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+ txtPatient.LostFocus.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
menuExit.Click.AddHandler(fun obj args ->
askSaveCurrent ()
let res = dialog.ShowDialog()
if res.HasValue && res.Value
then
- let srcImg = state.AddSourceImage(dialog.FileName)
+ let srcImg = state.AddSourceImage dialog.FileName defaultConfig
addPreview srcImg
updateGlobalParasitemia ()
if state.SourceImages.Count() = 1
updateCurrentImage ())
menuStartAnalysis.Click.AddHandler(fun obj args ->
- if Analysis.showWindow mainWindow.Root state defaultConfig
+ if Analysis.showWindow mainWindow.Root state
then
updateGlobalParasitemia ()
updateCurrentImage ())
<Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Margin="0,0,3,3" CornerRadius="5" >
<TextBlock x:Name="txtImageNumber" Padding="2" Text="42" Foreground="White" />
</Border>
- <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5"/>
+ <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5" Visibility="Hidden"/>
</Grid>
</Border>
</UserControl>
\ No newline at end of file
--- /dev/null
+<UserControl
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:fsxaml="clr-namespace:FsXaml;assembly=FsXaml.Wpf"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d" d:DesignWidth="349.723" d:DesignHeight="118.911"
+ >
+ <UserControl.Background>
+ <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}"/>
+ </UserControl.Background>
+ <Grid x:Name="gridMain">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition/>
+ </Grid.ColumnDefinitions>
+ <Grid x:Name="gridImage" Grid.ColumnSpan="1" VerticalAlignment="Top">
+ <Image x:Name="imagePreview" />
+ <CheckBox x:Name="chkSelection" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="3,3,0,0"/>
+ <Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Background="#4C000000" Margin="0,0,3,3" CornerRadius="5" >
+ <TextBlock x:Name="txtImageNumber" Padding="2" Text="42" Foreground="White" />
+ </Border>
+ <Rectangle x:Name="viewport" Margin="24,30,71,26" Stroke="#BFFFFF00" RenderTransformOrigin="0.5,0.5" Visibility="Hidden"/>
+ </Grid>
+ <Grid Grid.Column="1">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="1*"/>
+ </Grid.RowDefinitions>
+ <Label Content="Last analysis" Grid.Column="0" Grid.Row="0" Margin="10,0,3,0" />
+ <Label Content="Resolution [PPI]" Grid.Column="0" Grid.Row="1" Margin="10,0,3,0" />
+ <Label x:Name="lblDateLastAnalysis" Grid.Column="1" Margin="3,0,3,0"/>
+ <Grid Grid.Column="1" Grid.Row="1">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <TextBox x:Name="txtResolution" Margin="3" Text="" Grid.Column="0" />
+ <Button x:Name="butDefaultResolutions" Content="Defaults" Grid.Column="1" Margin="3">
+ <Button.ContextMenu>
+ <ContextMenu>
+ <MenuItem x:Name="menuZoom50X" Header="_200'000 PPI (50X)" />
+ <MenuItem x:Name="menuZoom100X" Header="_400'000 PPI (100X)" />
+ </ContextMenu>
+ </Button.ContextMenu>
+ <Button.Style>
+ <Style TargetType="{x:Type Button}">
+ <Style.Triggers>
+ <EventTrigger RoutedEvent="Click">
+ <EventTrigger.Actions>
+ <BeginStoryboard>
+ <Storyboard>
+ <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
+ <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
+ </BooleanAnimationUsingKeyFrames>
+ </Storyboard>
+ </BeginStoryboard>
+ </EventTrigger.Actions>
+ </EventTrigger>
+ </Style.Triggers>
+ </Style>
+ </Button.Style>
+ </Button>
+ </Grid>
+ </Grid>
+ </Grid>
+</UserControl>
\ No newline at end of file
--- /dev/null
+namespace Parasitemia.GUI.Views
+
+open System
+open System.Windows
+open System.Windows.Data
+open System.Windows.Input
+
+open FSharp.ViewModule
+open FsXaml
+
+type ImageSourceSelection = XAML<"GUI/ImageSourceSelection.xaml", true>
+
+(* type ImageSourcePreviewController() =
+ inherit UserControlViewController<ImageSourcePreview>() *)
+
+(* type ImageSourcePreviewViewModel() =
+ inherit ViewModelBase() *)
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
- <Label x:Name="lblPatient" Margin="10, 0, 3, 0 " Content="Patient ID" Grid.ColumnSpan="2"/>
- <Label x:Name="lblGlobalParasitemia" Margin="10, 0, 3, 0" Content="Global parasitemia" Grid.Row="1" Grid.ColumnSpan="2" />
+ <Label x:Name="lblPatient" Margin="10,0,3,0 " Content="Patient ID" Grid.ColumnSpan="2"/>
+ <Label x:Name="lblGlobalParasitemia" Margin="10,0,3,0" Content="Global parasitemia" Grid.Row="1" Grid.ColumnSpan="2" />
<TextBox x:Name="txtPatient" Grid.Column="2" Margin="3,4,10,4" TextWrapping="Wrap" VerticalAlignment="Center" />
<TextBox x:Name="txtGlobalParasitemia" Grid.Column="2" Grid.Row="1" Margin="3,4,10,4" TextWrapping="Wrap" VerticalAlignment="Center" IsReadOnly="True" />
</Grid>
open System.IO
open System.IO.Compression
-open FSharp.Data
-
open Emgu.CV
open Emgu.CV.Structure
+open Newtonsoft.Json
+open Newtonsoft.Json.Converters
+
open Types
let extension = ".piaz"
let filter = "PIA|*.piaz"
+// Information associated to a document.
+type JSONInformation = {
+ patientID: string
+}
+
+// Information associated to each images.
+type JSONSourceImage = {
+ num: int
+ parameters: Config.Parameters
+ dateLastAnalysis: DateTime
+ rbcs: RBC List
+}
+
type FileData = {
- sources: SourceImage list
- patientID: string }
-
-// The json type associated to a source image.
-type JSONSourceImage = JsonProvider<"""
- {
- "RBCRadius" : 32.5,
- "dateLastAnalysis" : 1.5,
- "rbcs": [
- {
- "num": 1,
- "infected": true,
- "setManually": false,
- "posX" : 42.5,
- "posY" : 42.5,
- "width" : 10.5,
- "height" : 10.5,
- "stainArea" : 10
- }
- ]
- }
-""">
-
-// The json type associated to a file.
-type JSONMainInformation = JsonProvider<"""
- {
- "patientID": "1234abcd"
- }
-""">
-
-let mainFilename = "info.json"
+ patientID: string
+ images: SourceImage list
+}
+
+let mainEntryName = "info.json"
let imageExtension = ".tiff"
let save (filePath: string) (data: FileData) =
e.Delete()
// Main JSON file.
- let mainJSON = JSONMainInformation.Root(data.patientID)
- let mainFile = file.CreateEntry(mainFilename, CompressionLevel.Fastest)
- use mainFileWriter = new StreamWriter(mainFile.Open())
- mainJSON.JsonValue.WriteTo(mainFileWriter, JsonSaveOptions.None)
+ let mainEntry = file.CreateEntry(mainEntryName, CompressionLevel.Fastest)
+ use mainEntryWriter = new StreamWriter(mainEntry.Open())
+ mainEntryWriter.Write(JsonConvert.SerializeObject({ JSONInformation.patientID = data.patientID }))
// Write each images and the associated information.
- for imgSrc in data.sources do
- let imgFilename = (string imgSrc.num) + imageExtension
+ for srcImg in data.images do
+ let imgFilename = (string srcImg.num) + imageExtension
let imgEntry = file.CreateEntry(imgFilename, CompressionLevel.NoCompression) // FIXME: It seems a compression is applied to this file despite of the 'NoCompression' flag.
- imgSrc.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
-
- let imgJSON =
- JSONSourceImage.Root(decimal imgSrc.rbcRadius,
- decimal <| imgSrc.dateLastAnalysis.ToFileTimeUtc(),
- [| for rbc in imgSrc.rbcs ->
- JSONSourceImage.Rbc(
- rbc.num,
- rbc.infected, rbc.setManually,
- decimal rbc.center.X, decimal rbc.center.Y, decimal rbc.size.Width, decimal rbc.size.Height,
- rbc.infectedArea) |])
+ srcImg.img.ToBitmap().Save(imgEntry.Open(), System.Drawing.Imaging.ImageFormat.Tiff)
let imgJSONEntry = file.CreateEntry(imgFilename + ".json", CompressionLevel.Fastest)
use imgJSONFileWriter = new StreamWriter(imgJSONEntry.Open())
- imgJSON.JsonValue.WriteTo(imgJSONFileWriter, JsonSaveOptions.None)
+ imgJSONFileWriter.Write(JsonConvert.SerializeObject({ num = srcImg.num; parameters = srcImg.config.Parameters; dateLastAnalysis = srcImg.dateLastAnalysis; rbcs = srcImg.rbcs }))
let load (filePath: string) : FileData =
use file = ZipFile.Open(filePath, ZipArchiveMode.Read)
- let mainFile = file.GetEntry(mainFilename)
- let mainJSON = JSONMainInformation.Load(mainFile.Open())
-
- let sources = [
- let mutable imgNum = 0
- for imgEntry in file.Entries do
- let filename = imgEntry.Name
- if filename.EndsWith(imageExtension)
- then
- let img = new Image<Bgr, byte>(new System.Drawing.Bitmap(imgEntry.Open(), false)) // FIXME: Should we dispose the bitmap?
- imgNum <- imgNum + 1
- let imgJSONEntry = file.GetEntry(filename + ".json")
- let imgJSON = JSONSourceImage.Load(imgJSONEntry.Open())
- yield { num = imgNum
- rbcRadius = float imgJSON.RbcRadius
- dateLastAnalysis = DateTime.FromFileTimeUtc(int64 imgJSON.DateLastAnalysis)
- img = img
- rbcs = [ for rbc in imgJSON.Rbcs ->
- { 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);
- infectedArea = rbc.StainArea } ] } ]
- { sources = sources; patientID = mainJSON.PatientId }
\ No newline at end of file
+ let mainEntry = file.GetEntry(mainEntryName)
+ use mainEntryReader = new StreamReader(mainEntry.Open())
+ let info = JsonConvert.DeserializeObject<JSONInformation>(mainEntryReader.ReadToEnd())
+
+ { patientID = info.patientID
+ images = [ let mutable imgNum = 0
+ for imgEntry in file.Entries do
+ if imgEntry.Name.EndsWith(imageExtension)
+ then
+ let img = new Image<Bgr, byte>(new System.Drawing.Bitmap(imgEntry.Open(), false)) // FIXME: Should we dispose the bitmap?
+ imgNum <- imgNum + 1
+ let imgEntry = file.GetEntry(imgEntry.Name + ".json")
+ use imgEntryFileReader = new StreamReader(imgEntry.Open())
+ let imgInfo = JsonConvert.DeserializeObject<JSONSourceImage>(imgEntryFileReader.ReadToEnd())
+ yield { num = imgNum
+ config = Config.Config(imgInfo.parameters)
+ dateLastAnalysis = imgInfo.dateLastAnalysis
+ img = img
+ rbcs = imgInfo.rbcs } ] }
\ No newline at end of file
rbc.setManually <- not rbc.setManually
member this.Save () =
- let data = { PiaZ.sources = List.ofSeq sourceImages; PiaZ.patientID = this.PatientID }
+ let data = { PiaZ.FileData.patientID = this.PatientID; PiaZ.FileData.images = List.ofSeq sourceImages }
PiaZ.save this.FilePath data
alteredSinceLastSave <- false
let data = PiaZ.load this.FilePath
this.PatientID <- data.patientID
sourceImages.Clear()
- sourceImages.InsertRange(0, data.sources)
+ sourceImages.InsertRange(0, data.images)
if sourceImages.Count > 0
then this.CurrentImage <- Some sourceImages.[0]
alteredSinceLastSave <- false
- member this.AddSourceImage (filePath: string) : SourceImage =
- let srcImg = { num = sourceImages.Count + 1; rbcRadius = 0.; dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
+ member this.AddSourceImage (filePath: string) (defaultConfig: Config.Config) : SourceImage =
+ let srcImg = { num = sourceImages.Count + 1; config = defaultConfig.Copy(); dateLastAnalysis = DateTime(0L); rbcs = []; img = new Image<Bgr, byte>(filePath) }
sourceImages.Add(srcImg)
if sourceImages.Count = 1
then this.CurrentImage <- Some sourceImages.[0]
// Re-numbered the images.
sourceImages |> Seq.iteri (fun i srcImg -> srcImg.num <- i + 1)
- member this.SetResult (imgNum: int) (rbcRadius: float) (cells: Cell list) =
+ 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.rbcRadius <- rbcRadius
sourceImage.dateLastAnalysis <- DateTime.UtcNow
// To match with previously manually altered RBC.
let manuallyAlteredPreviousRBCS = sourceImage.rbcs |> List.filter (fun rbc -> rbc.setManually)
- let tolerance = rbcRadius * 0.5 // +/-.
+ let tolerance = (float sourceImage.config.RBCRadius) * 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)
type SourceImage = {
mutable num: int
- mutable rbcRadius: float
+ mutable config: Config.Config
mutable dateLastAnalysis: DateTime // UTC.
img: Image<Bgr, byte>
mutable rbcs: RBC list }
\ No newline at end of file
module ImageAnalysis
open System
+open System.Linq
open System.Drawing
open FSharp.Collections.ParallelSeq
| Some f -> f percent
| _ -> ()
+ let inline buildLogWithName (text: string) = sprintf "(%s) %s" name text
+ let logWithName = buildLogWithName >> log
+ let inline logTimeWithName (text: string) (f: unit -> 'a) : 'a = logTime (buildLogWithName text) f
+
+ logWithName "Starting analysis ..."
+
use green = img.Item(1)
let greenFloat = green.Convert<Gray, float32>()
let filteredGreen = gaussianFilter greenFloat config.LPFStandardDeviation
- let initialAreaOpening = int<| config.RBCArea * config.Parameters.ratioAreaPaleCenter
- logTime "Area opening number one" (fun () -> ImgTools.areaOpenF filteredGreen initialAreaOpening)
+ logWithName (sprintf "Nominal erytrocyte diameter: %s" config.FormattedRadius)
+
+ 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.
+ 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 r1 = logTime "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) range 1.0 |> float32)
- let r2 = logTime "Granulometry (area)" (fun() -> Granulometry.findRadiusByAreaClosing filteredGreen range |> float32)
+ //let r1 = log "Granulometry (morpho)" (fun() -> Granulometry.findRadiusByClosing (filteredGreen.Convert<Gray, byte>()) range 1.0 |> float32)
+ let r2 = 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)
+
report 24
let secondAreaOpening = int <| config.RBCArea * config.Parameters.ratioAreaPaleCenter
if secondAreaOpening > initialAreaOpening
then
- logTime "Area opening number two" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening)
+ logTimeWithName "Area opening number two" (fun () -> ImgTools.areaOpenF filteredGreen secondAreaOpening)
let parasites, filteredGreenWhitoutStain = ParasitesMarker.find filteredGreen config
//let parasites, filteredGreenWhitoutInfection, filteredGreenWhitoutStain = ParasitesMarker.findMa greenFloat filteredGreenFloat config
- let edges, xGradient, yGradient = logTime "Finding edges" (fun () -> ImgTools.findEdges filteredGreenWhitoutStain)
- logTime "Removing small connected components from thinning" (fun () -> removeArea edges (config.RBCRadius ** 2.f / 50.f |> int))
+ let edges, xGradient, yGradient = logTimeWithName "Finding edges" (fun () ->
+ let edges, xGradient, yGradient = ImgTools.findEdges filteredGreenWhitoutStain
+ removeArea edges (config.RBCRadius ** 2.f / 50.f |> int)
+ edges, xGradient, yGradient)
- let allEllipses, ellipses = logTime "Finding ellipses" (fun () ->
+ let allEllipses, ellipses = logTimeWithName "Finding ellipses" (fun () ->
let matchingEllipses = Ellipse.find edges xGradient yGradient config
matchingEllipses.Ellipses, matchingEllipses.PrunedEllipses)
+
report 80
- let cells = logTime "Classifier" (fun () -> Classifier.findCells ellipses parasites filteredGreenWhitoutStain config)
+ let cells = logTimeWithName "Classifier" (fun () -> Classifier.findCells ellipses parasites filteredGreenWhitoutStain config)
+
report 100
+ logWithName "Analysis finished"
+
// Output pictures if debug flag is set.
match config.Debug with
| DebugOn output ->
cells
-// ID * cell radius * cell list.
-let doMultipleAnalysis (imgs: (string * Image<Bgr, byte>) list) (config : Config) (reportProgress: (int -> unit) option) : (string * float * Cell list) list =
+// ID * cell list.
+let doMultipleAnalysis (imgs: (string * Config * Image<Bgr, byte>) list) (reportProgress: (int -> unit) option) : (string * Cell list) list =
let inline report (percent: int) =
match reportProgress with
| Some f -> f percent
| _ -> ()
- let monitor = Object()
- let mutable total = 0
+ let progressPerAnalysis = System.Collections.Concurrent.ConcurrentDictionary<string, int>()
let nbImgs = List.length imgs
- let reportProgressImg =
- (fun progress ->
- lock monitor (fun () -> total <- total + progress)
- report (total / nbImgs))
+
+ let reportProgressImg (id: string) (progress: int) =
+ progressPerAnalysis.AddOrUpdate(id, progress, (fun _ _ -> progress)) |> ignore
+ report (progressPerAnalysis.Values.Sum() / nbImgs)
let nbConcurrentTaskLimit = 4
let n = Environment.ProcessorCount
imgs
- |> PSeq.map (fun (id, img) ->
- let localConfig = config.Copy()
- let cells = doAnalysis img id localConfig (Some reportProgressImg)
- id, float localConfig.RBCRadius, cells)
+ |> PSeq.map (fun (id, config, img) -> id, doAnalysis img id config (Some (fun p -> reportProgressImg id p)))
|> PSeq.withDegreeOfParallelism (if n > nbConcurrentTaskLimit then nbConcurrentTaskLimit else n)
|> PSeq.toList
<Compile Include="GUI\NumericUpDown.xaml.fs" />
<Resource Include="GUI\ImageSourcePreview.xaml" />
<Compile Include="GUI\ImageSourcePreview.xaml.fs" />
+ <Resource Include="GUI\ImageSourceSelection.xaml" />
+ <Compile Include="GUI\ImageSourceSelection.xaml.fs" />
<Resource Include="GUI\RBCFrame.xaml" />
<Compile Include="GUI\RBCFrame.xaml.fs" />
<Resource Include="GUI\AnalysisWindow.xaml" />
<HintPath>..\packages\FSharp.Core.4.0.0.1\lib\net40\FSharp.Core.dll</HintPath>
<Private>True</Private>
</Reference>
- <Reference Include="FSharp.Data">
- <HintPath>..\packages\FSharp.Data.2.2.5\lib\net40\FSharp.Data.dll</HintPath>
- <Private>True</Private>
- </Reference>
- <Reference Include="FSharp.Data.TypeProviders" />
<Reference Include="FSharp.ViewModule">
<HintPath>..\packages\FSharp.ViewModule.Core.0.9.9.2\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\FSharp.ViewModule.dll</HintPath>
<Private>True</Private>
<Private>True</Private>
</Reference>
<Reference Include="mscorlib" />
+ <Reference Include="Newtonsoft.Json">
+ <HintPath>..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+ <Private>True</Private>
+ </Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System" />
diff._ThresholdBinary(Gray(0.0), Gray(255.))
diff.Convert<Gray, byte>()
- let infectionMarker = marker filteredGreen filteredGreenWithoutInfection config.Parameters.infectionLevel
- let stainMarker = marker filteredGreenWithoutInfection filteredGreenWithoutStain config.Parameters.stainLevel
+ 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"
+ *)
{ darkStain = darkStain
infection = infectionMarker
use resultFile = new StreamWriter(new FileStream(Path.Combine(output, "results.txt"), FileMode.Append, FileAccess.Write))
//try
- let images = [ for file in files -> Path.GetFileNameWithoutExtension(FileInfo(file).Name), new Image<Bgr, byte>(file) ]
+ let images = [ for file in files -> Path.GetFileNameWithoutExtension(FileInfo(file).Name), config.Copy(), new Image<Bgr, byte>(file) ]
Utils.logTime "Whole analyze" (fun () ->
- let results = ImageAnalysis.doMultipleAnalysis images config None
+ let results = ImageAnalysis.doMultipleAnalysis images None
- for id, _, cells in results do
+ for id, cells in results do
let total, infected = Utils.countCells cells
fprintf resultFile "File: %s %d %d %.2f\n" id total infected (100. * (float infected) / (float total)))
module UnitsOfMeasure
-[<Measure>]
-type px
+[<Measure>] type px
+[<Measure>] type μm
+[<Measure>] type inch
+[<Measure>] type ppi = px / inch
-[<Measure>]
-type μm =
- static member ConvertToInch(x: float<μm>) : float<inch> =
- x * 1.<inch> / 25.4e3<μm>
+let μmInchRatio = 25.4e3<μm/inch>
-and [<Measure>] inch
+let μmToInch(x: float<μm>) : float<inch> = x / μmInchRatio
+let inchToμm(x: float<inch>) : float<μm> = x * μmInchRatio
-[<Measure>]
-type ppi = px / inch
<package id="log4net" version="2.0.5" targetFramework="net461" />
<package id="MathNet.Numerics" version="3.10.0" targetFramework="net461" />
<package id="MathNet.Numerics.FSharp" version="3.10.0" targetFramework="net461" />
+ <package id="Newtonsoft.Json" version="8.0.2" targetFramework="net452" />
</packages>
\ No newline at end of file