From 24bfd2ea10b5945100168ad5a1b2545e43d05569 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Tue, 20 Apr 2021 22:47:20 +0200 Subject: [PATCH] Save imported image in the same format (WIP) #313 --- Parasitemia/ParasitemiaUI/Constants.fs | 4 +- Parasitemia/ParasitemiaUI/GUI.fs | 12 ++++- Parasitemia/ParasitemiaUI/PiaZ.fs | 53 ++++++++++++------- Parasitemia/ParasitemiaUI/SourceImage.fs | 35 +++++++++++- Parasitemia/ParasitemiaUI/State.fs | 3 +- Parasitemia/ParasitemiaUI/Utils.fs | 1 + .../ParasitemiaUIControls/MainWindow.xaml | 2 +- 7 files changed, 85 insertions(+), 25 deletions(-) diff --git a/Parasitemia/ParasitemiaUI/Constants.fs b/Parasitemia/ParasitemiaUI/Constants.fs index bef3910..255c27b 100644 --- a/Parasitemia/ParasitemiaUI/Constants.fs +++ b/Parasitemia/ParasitemiaUI/Constants.fs @@ -3,10 +3,12 @@ open System open System.IO +let APPLICATION_NAME = "Parasitemia" + let USER_DIRECTORY = Path.Combine ( Environment.GetFolderPath Environment.SpecialFolder.ApplicationData, - "Parasitemia" + APPLICATION_NAME ) let USER_DIRECTORY_LOG = diff --git a/Parasitemia/ParasitemiaUI/GUI.fs b/Parasitemia/ParasitemiaUI/GUI.fs index 3b67894..0832a41 100644 --- a/Parasitemia/ParasitemiaUI/GUI.fs +++ b/Parasitemia/ParasitemiaUI/GUI.fs @@ -18,6 +18,7 @@ open Types let run (defaultConfig : Config) (fileToOpen : string option) = let app = new Application () let win = MainWindow () + win.Title <- Constants.APPLICATION_NAME let state = State.State defaultConfig let mutable currentScale = 1. @@ -401,6 +402,9 @@ let run (defaultConfig : Config) (fileToOpen : string option) = updateDocumentStatus () let loadFile (filepath : string) = + let displayLoadingErrorMessage (message : string) = + MessageBox.Show (sprintf "The document cannot be loaded from \"%s\": %s" filepath message, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore + askSaveCurrent () let previousFilePath = state.FilePath try @@ -408,10 +412,14 @@ let run (defaultConfig : Config) (fileToOpen : string option) = state.Load () updateGUI () with + | PiaZ.VersionFileNewerException fileVersion -> + state.FilePath <- previousFilePath + displayLoadingErrorMessage $"File version ({fileVersion}) is newer than supported one ({PiaZ.CURRENT_FILE_VERSION}), you may update Parasitemia to the latest version" + | :? IOException as ex -> Log.Error "%O" ex state.FilePath <- previousFilePath - MessageBox.Show (sprintf "The document cannot be loaded from \"%s\"" filepath, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore + displayLoadingErrorMessage "IO Error" let askLoadFile () = let dialog = OpenFileDialog (Filter = PiaZ.filter) @@ -443,7 +451,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) = MessageBox.Show (sprintf "The results cannot be exported in \"%s\"" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore let importImage () = - let dialog = OpenFileDialog (Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true) + let dialog = OpenFileDialog (Filter = "Image Files|*.png;*.jpg;.jpeg;.bmp;*.tif;*.tiff", Multiselect = true) let res = dialog.ShowDialog () if res.HasValue && res.Value then let noSourceImage = state.SourceImages.Count () = 0 diff --git a/Parasitemia/ParasitemiaUI/PiaZ.fs b/Parasitemia/ParasitemiaUI/PiaZ.fs index 18e9d6a..8498c64 100644 --- a/Parasitemia/ParasitemiaUI/PiaZ.fs +++ b/Parasitemia/ParasitemiaUI/PiaZ.fs @@ -43,9 +43,10 @@ type DocumentData = images : SourceImage list } -let mainEntryName = "info.json" -let imageExtension = ".tiff" -let currentFileVersion = 2 +let MAIN_ENTRY_NAME = "info.json" +let DEFAULT_IMAGE_EXTENSION = ".tiff" +let JSON_EXTENSION = ".json" +let CURRENT_FILE_VERSION = 3 /// /// Save a document in a give file path. The file may already exist. @@ -56,21 +57,31 @@ let currentFileVersion = 2 let save (filePath : string) (data : DocumentData) = use file = ZipFile.Open (filePath, ZipArchiveMode.Update) + // We only delete JSON files and removed images. for e in List.ofSeq file.Entries do // 'ofSeq' to not iterate a collection currently modified. - e.Delete () + if Path.GetExtension e.Name = JSON_EXTENSION || data.images |> List.exists (fun img -> img.OriginalName = e.Name) |> not then + e.Delete () // Main JSON file. - let mainEntry = file.CreateEntry (mainEntryName, CompressionLevel.Fastest) + let mainEntry = file.CreateEntry (MAIN_ENTRY_NAME, CompressionLevel.Fastest) use mainEntryWriter = new StreamWriter (mainEntry.Open ()) - mainEntryWriter.Write (JsonConvert.SerializeObject ({ patientID = data.patientID; fileVersion = currentFileVersion })) + mainEntryWriter.Write (JsonConvert.SerializeObject ({ patientID = data.patientID; fileVersion = CURRENT_FILE_VERSION })) - // Write each images and the associated information. + // Write each images and the associated information as a JSON file. 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. - srcImg.Img.ToBitmap().Save (imgEntry.Open (), System.Drawing.Imaging.ImageFormat.Tiff) + match srcImg.TempFile with + | Some imgTempFile -> + let imgEntry = file.CreateEntry (srcImg.OriginalName, CompressionLevel.NoCompression) + (File.Open (imgTempFile, FileMode.Open, FileAccess.Read)).CopyTo (imgEntry.Open ()) + srcImg.TempFile <- None - let imgJSONEntry = file.CreateEntry (imgFilename + ".json", CompressionLevel.Fastest) + | None -> () + + //let imgFilename = (string srcImg.Num) + DEFAULT_IMAGE_EXTENSION + //let imgEntry = file.CreateEntry (imgFilename, CompressionLevel.NoCompression) + //srcImg.Img.ToBitmap().Save (imgEntry.Open (), System.Drawing.Imaging.ImageFormat.Tiff) + + let imgJSONEntry = file.CreateEntry (srcImg.OriginalName + JSON_EXTENSION, CompressionLevel.Fastest) use imgJSONFileWriter = new StreamWriter (imgJSONEntry.Open ()) imgJSONFileWriter.Write ( JsonConvert.SerializeObject ( @@ -95,33 +106,38 @@ let updateDocumentData (fromVersion : int) (toVersion : int) (data : DocumentDat | _ -> () data +exception VersionFileNewerException of int + /// /// Load document from a give file path. /// /// Path to the PiaZ file /// /// If the file cannot be read +/// If the file version is newer than the current supported version let load (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : DocumentData = use file = ZipFile.Open (filePath, ZipArchiveMode.Read) - let mainEntry = file.GetEntry (mainEntryName) + let mainEntry = file.GetEntry (MAIN_ENTRY_NAME) use mainEntryReader = new StreamReader (mainEntry.Open ()) let info = JsonConvert.DeserializeObject (mainEntryReader.ReadToEnd ()) - updateDocumentData info.fileVersion currentFileVersion + if info.fileVersion > CURRENT_FILE_VERSION then + raise <| VersionFileNewerException info.fileVersion + + updateDocumentData info.fileVersion CURRENT_FILE_VERSION { patientID = info.patientID images = [ - let mutable imgNum = 0 for imgEntry in file.Entries do - if imgEntry.Name.EndsWith (imageExtension) then + if imgEntry.Name.EndsWith JSON_EXTENSION |> not then use bitmap = new System.Drawing.Bitmap (imgEntry.Open (), false) let img = bitmap.ToImage () - imgNum <- imgNum + 1 - let imgJSONEntry = file.GetEntry (imgEntry.Name + ".json") + let imgJSONEntry = file.GetEntry (imgEntry.Name + JSON_EXTENSION) use imgJSONFileReader = new StreamReader (imgJSONEntry.Open ()) let imgInfo = JsonConvert.DeserializeObject (imgJSONFileReader.ReadToEnd ()) + let imgNum = imgInfo.num let config = defaultConfig.Copy () config.Parameters <- @@ -132,6 +148,7 @@ let load (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : D config.SetRBCRadius imgInfo.RBCRadius - SourceImage (imgNum, imgInfo.name, config, imgInfo.dateLastAnalysis, img, imgInfo.rbcs, HealthyRBCBrightness = imgInfo.healthyRBCBrightness, InfectedRBCBrightness = imgInfo.infectedRBCBrightness) + SourceImage (imgNum, imgInfo.name, imgEntry.Name, config, imgInfo.dateLastAnalysis, FromMemory img, imgInfo.rbcs, HealthyRBCBrightness = imgInfo.healthyRBCBrightness, InfectedRBCBrightness = imgInfo.infectedRBCBrightness) ] + |> List.sortBy (fun image -> image.Num) } \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/SourceImage.fs b/Parasitemia/ParasitemiaUI/SourceImage.fs index 7f5c245..8dd20df 100644 --- a/Parasitemia/ParasitemiaUI/SourceImage.fs +++ b/Parasitemia/ParasitemiaUI/SourceImage.fs @@ -1,6 +1,7 @@ namespace ParasitemiaUI open System +open System.IO open System.Windows.Media open Emgu.CV @@ -8,12 +9,36 @@ open Emgu.CV.Structure open Types -type SourceImage (num : int, name : string, config : ParasitemiaCore.Config.Config, dateLastAnalysis : DateTime, img : Image, rbcs : RBC list) = +type ImageData = + | FromMemory of Image + | FromFile of string // This file will be stored in a temporary directory until the image is saved in a piaz file. + +type SourceImage (num : int, name : string, originalName : string, config : ParasitemiaCore.Config.Config, dateLastAnalysis : DateTime, imgData : ImageData, rbcs : RBC list) = let mutable num = num let mutable name = name let mutable config = config let mutable dateLastAnalysis = dateLastAnalysis // UTC. - let img = img + let img = + match imgData with + | FromMemory i -> i + | FromFile path -> new Image (path) + + let mutable tempFile : string option = + match imgData with + | FromMemory _ -> None + | FromFile path -> + let tmpDirname = Guid.NewGuid () + let filename = Path.GetFileName path + + // Copy it to a temporary directory. + let tmpDir = Path.Combine (Path.GetTempPath (), string tmpDirname) + Directory.CreateDirectory tmpDir |> ignore + let tmpPath = Path.Combine (tmpDir, filename) + + File.Copy (path, tmpPath) + + Some tmpPath + let mutable rbcs = rbcs let mutable healthyRBCBrightness = 1.f let mutable infectedRBCBrightness = 1.f @@ -39,12 +64,18 @@ type SourceImage (num : int, name : string, config : ParasitemiaCore.Config.Conf member this.Name with get () = name and set value = name <- value + member this.OriginalName = originalName + member this.Config = config member this.DateLastAnalysis with get () = dateLastAnalysis and set value = dateLastAnalysis <- value member this.Img = img + member this.TempFile + with get () = tempFile + and set value = tempFile <- value // TODO: remove temp file if set to None + member this.RBCs with get () = rbcs and set value = diff --git a/Parasitemia/ParasitemiaUI/State.fs b/Parasitemia/ParasitemiaUI/State.fs index befbdec..37da042 100644 --- a/Parasitemia/ParasitemiaUI/State.fs +++ b/Parasitemia/ParasitemiaUI/State.fs @@ -64,7 +64,8 @@ type State (defaultConfig : ParasitemiaCore.Config.Config) = /// /// If the image cannot be read member this.AddSourceImage (filePath : string) (defaultConfig : ParasitemiaCore.Config.Config) : SourceImage = - let srcImg = SourceImage (sourceImages.Count + 1, System.IO.FileInfo(filePath).Name, defaultConfig.Copy (), DateTime (0L), new Image (filePath), []) + let filename = System.IO.FileInfo(filePath).Name + let srcImg = SourceImage (sourceImages.Count + 1, filename, filename, defaultConfig.Copy (), DateTime (0L), FromFile filePath, []) sourceImages.Add srcImg if sourceImages.Count = 1 then this.CurrentImage <- Some sourceImages.[0] diff --git a/Parasitemia/ParasitemiaUI/Utils.fs b/Parasitemia/ParasitemiaUI/Utils.fs index 927e441..a536853 100644 --- a/Parasitemia/ParasitemiaUI/Utils.fs +++ b/Parasitemia/ParasitemiaUI/Utils.fs @@ -108,6 +108,7 @@ let argsHelp = open System open System.Windows +// Inspired by https://github.com/fsprojects/FSharp.ViewModule/blob/master/src/FSharp.ViewModule/FunCommand.fs type FunCommand (execute : obj -> unit, canExecute : obj -> bool) = let canExecuteChanged = Event () diff --git a/Parasitemia/ParasitemiaUIControls/MainWindow.xaml b/Parasitemia/ParasitemiaUIControls/MainWindow.xaml index 142cc58..c6367af 100644 --- a/Parasitemia/ParasitemiaUIControls/MainWindow.xaml +++ b/Parasitemia/ParasitemiaUIControls/MainWindow.xaml @@ -6,7 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Height="700" Width="1000" MinHeight="300" MinWidth="400" Title="Parasitemia" Icon="/ParasitemiaUIControls;component/Resources/icon.ico" ResizeMode="CanResizeWithGrip" + Height="700" Width="1000" MinHeight="300" MinWidth="400" Icon="/ParasitemiaUIControls;component/Resources/icon.ico" ResizeMode="CanResizeWithGrip" > -- 2.43.0