diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 08e4d6df..5b1435b7 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -4,8 +4,8 @@ on: [push, pull_request] env: BUILD_TYPE: "Release" - OPENCV_VERSION: "3.4.13" - EDDL_VERSION: "v0.8.3a" + OPENCV_VERSION: "3.4.14" + EDDL_VERSION: "v0.9.2b" PROC: 2 jobs: @@ -14,8 +14,8 @@ jobs: strategy: matrix: cfg: - - { os: ubuntu-18.04, c-version: gcc-6, cxx-version: g++-6, generator: "Unix Makefiles" } - - { os: ubuntu-18.04, c-version: gcc-10, cxx-version: g++-10, generator: "Unix Makefiles" } + - { os: ubuntu-18.04, c-version: gcc-7, cxx-version: g++-7, generator: "Unix Makefiles" } + - { os: ubuntu-18.04, c-version: gcc-11, cxx-version: g++-11, generator: "Unix Makefiles" } - { os: ubuntu-18.04, c-version: clang-5.0, cxx-version: clang++-5.0, generator: "Unix Makefiles" } - { os: ubuntu-18.04, c-version: clang-10, cxx-version: clang++-10, generator: "Unix Makefiles" } steps: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index f912bf29..2a509013 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -4,8 +4,8 @@ on: [push, pull_request] env: BUILD_TYPE: "Release" - OPENCV_VERSION: "3.4.13" - EDDL_VERSION: "v0.8.3a" + OPENCV_VERSION: "3.4.14" + EDDL_VERSION: "v0.9.2b" PROC: 2 jobs: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 11b124a5..aa3cfcc3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,8 +4,8 @@ on: [push, pull_request] env: BUILD_TYPE: "Release" - OPENCV_VERSION: "3.4.13" - EDDL_VERSION: "v0.8.3a" + OPENCV_VERSION: "3.4.14" + EDDL_VERSION: "v0.9.2b" PROC: 2 jobs: diff --git a/3rdparty/dcmtk/CMakeLists.txt b/3rdparty/dcmtk/CMakeLists.txt index 782d207f..7154da1f 100644 --- a/3rdparty/dcmtk/CMakeLists.txt +++ b/3rdparty/dcmtk/CMakeLists.txt @@ -6,6 +6,9 @@ FetchContent_Declare( if(ECVL_WITH_DICOM) if(ECVL_BUILD_DEPS) + if(POLICY CMP0115) + set(CMAKE_POLICY_DEFAULT_CMP0115 OLD) + endif() FetchContent_GetProperties(dcmtk) if(NOT dcmtk_POPULATED) FetchContent_Populate(dcmtk) diff --git a/CMakeLists.txt b/CMakeLists.txt index 899c6327..57af48e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "") set(CMAKE_POSITION_INDEPENDENT_CODE ON) # To always generate position independent code +set(CMAKE_VERBOSE_MAKEFILE ON) if (WIN32) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif() diff --git a/Jenkinsfile b/Jenkinsfile index b980df82..fd17b2c8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -15,7 +15,7 @@ pipeline { steps { timeout(60) { echo 'Building..' - cmakeBuild buildDir: 'build', cmakeArgs: '-DECVL_TESTS=ON -DECVL_BUILD_EDDL=ON -DECVL_DATASET=ON -DECVL_WITH_DICOM=ON -DECVL_WITH_OPENSLIDE=ON -DECVL_GPU=OFF', installation: 'InSearchPath', sourceDir: '.', cleanBuild: true, steps: [ + cmakeBuild buildDir: 'build', buildType: 'Release', cmakeArgs: '-DECVL_TESTS=ON -DECVL_BUILD_EDDL=ON -DECVL_DATASET=ON -DECVL_WITH_DICOM=ON -DECVL_WITH_OPENSLIDE=ON -DECVL_GPU=OFF', installation: 'InSearchPath', sourceDir: '.', cleanBuild: true, steps: [ [args: '--parallel 4', withCmake: true] ] } @@ -25,7 +25,7 @@ pipeline { steps { timeout(15) { echo 'Testing..' - ctest arguments: '-C Debug -VV', installation: 'InSearchPath', workingDir: 'build' + ctest arguments: '-C Release -VV', installation: 'InSearchPath', workingDir: 'build' } } } @@ -46,8 +46,8 @@ pipeline { timeout(60) { echo 'Building..' bat 'powershell ../../ecvl_dependencies/ecvl_dependencies.ps1' - cmakeBuild buildDir: 'build', cmakeArgs: '-DECVL_TESTS=ON -DECVL_BUILD_EDDL=ON -DECVL_DATASET=ON -DECVL_WITH_DICOM=ON -DECVL_WITH_OPENSLIDE=ON -DOPENSLIDE_LIBRARIES=C:/Library/openslide-win32-20171122/lib/libopenslide.lib', installation: 'InSearchPath', sourceDir: '.', cleanBuild: true, steps: [ - [args: '--parallel 4', withCmake: true] + cmakeBuild buildDir: 'build', buildType: 'Release', cmakeArgs: '-DECVL_TESTS=ON -DECVL_BUILD_EDDL=ON -DECVL_DATASET=ON -DECVL_WITH_DICOM=ON -DECVL_WITH_OPENSLIDE=ON -DOPENSLIDE_LIBRARIES=C:/Library/openslide-win32-20171122/lib/libopenslide.lib', installation: 'InSearchPath', sourceDir: '.', cleanBuild: true, steps: [ + [args: '--config Release --parallel 4', withCmake: true] ] } } @@ -56,7 +56,7 @@ pipeline { steps { timeout(15) { echo 'Testing..' - bat 'cd build && ctest -C Debug -VV' + bat 'cd build && ctest -C Release -VV' } } } @@ -64,7 +64,7 @@ pipeline { steps { timeout(15) { echo 'Calculating coverage..' - bat '"C:/Program Files/OpenCppCoverage/OpenCppCoverage.exe" --source %cd% --export_type=cobertura --excluded_sources=3rdparty -- "build/bin/Debug/ECVL_TESTS.exe"' + bat '"C:/Program Files/OpenCppCoverage/OpenCppCoverage.exe" --source %cd% --export_type=cobertura --excluded_sources=3rdparty -- "build/bin/Release/ECVL_TESTS.exe"' cobertura coberturaReportFile: 'ECVL_TESTSCoverage.xml' bat 'codecov -f ECVL_TESTSCoverage.xml -t 7635bd2e-51cf-461e-bb1b-fc7ba9fb26d1' } diff --git a/README.md b/README.md index 494ef84a..181b6457 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The ECVL documentation is available [here](https://deephealthproject.github.io/e ## Requirements - CMake 3.13 or later -- C++ Compiler with C++17 support (e.g. GCC 6 or later, Clang 5.0 or later, Visual Studio 2017 or later) +- C++ Compiler with C++17 support (e.g. GCC 7 or later, Clang 5.0 or later, Visual Studio 2017 or later) - [OpenCV](https://opencv.org) 3.0 or later (modules required: `core`, `imgproc`, `imgcodecs`, `photo`, [`calib3d` since OpenCV 4.0 only. Note that `calib3d` depends on `features2d` and `flann`]) ### Optional @@ -193,16 +193,16 @@ Contributions of any kind are welcome! Windows Server 2016 VS 2017 15.9.28307 - 3.4.13 - 0.8.3 + 3.4.14 + 0.9.2b GitHub Actions Workflow status badge Windows Server 2019 - VS 2019 16.8.30804 - 3.4.13 - 0.8.3 + VS 2019 16.9.31229 + 3.4.14 + 0.9.2b GitHub Actions @@ -228,31 +228,31 @@ Contributions of any kind are welcome! Ubuntu 18.04.5 - GCC 6.5.0 - 3.4.13 - 0.8.3 + GCC 7.5.0 + 3.4.14 + 0.9.2b GitHub Actions Workflow status badge Ubuntu 18.04.5 - GCC 10.1.0 - 3.4.13 - 0.8.3 + GCC 11.1.0 + 3.4.14 + 0.9.2b GitHub Actions Ubuntu 18.04.5 Clang 5.0.1 - 3.4.13 - 0.8.3 + 3.4.14 + 0.9.2b GitHub Actions Ubuntu 18.04.5 Clang 10.0.0 - 3.4.13 - 0.8.3 + 3.4.14 + 0.9.2b GitHub Actions @@ -273,8 +273,8 @@ Contributions of any kind are welcome! macOS 10.15 Apple Clang 12.0.0 - 3.4.13 - 0.8.3 + 3.4.14 + 0.9.2b GitHub Actions Workflow status badge diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 51b5a876..14a49dec 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -38,8 +38,11 @@ if(ECVL_DATASET) endif() if(ECVL_BUILD_EDDL AND eddl_FOUND) add_executable(example_ecvl_eddl "examples/example_ecvl_eddl.cpp") + add_executable(example_pipeline "examples/example_pipeline.cpp") set_target_properties(example_ecvl_eddl PROPERTIES FOLDER "Examples") target_link_libraries(example_ecvl_eddl ${ECVL_MODULES}) + set_target_properties(example_pipeline PROPERTIES FOLDER "Examples") + target_link_libraries(example_pipeline ${ECVL_MODULES}) endif() if(ECVL_BUILD_GUI AND wxWidgets_FOUND) add_executable(example_ecvl_gui "examples/example_ecvl_gui.cpp") diff --git a/examples/example_dataset_generator.cpp b/examples/example_dataset_generator.cpp index b42acb09..57d4a5be 100644 --- a/examples/example_dataset_generator.cpp +++ b/examples/example_dataset_generator.cpp @@ -45,17 +45,18 @@ int main() vector mask; vector black; - for (auto& index : d_segmentation.split_.training_) { - if (d_segmentation.samples_[index].label_path_.value().filename().compare("black.png") == 0) { - black.emplace_back(index); + auto& training = d_segmentation.GetSplit("training"); + for (auto& sample_index : training) { + if (d_segmentation.samples_[sample_index].label_path_.value().filename().compare("black.png") == 0) { + black.emplace_back(sample_index); } else { - mask.emplace_back(index); + mask.emplace_back(sample_index); } } - d_segmentation.split_.training_.clear(); - d_segmentation.split_.training_.insert(d_segmentation.split_.training_.end(), mask.begin(), mask.end()); + training.clear(); + training.insert(training.end(), mask.begin(), mask.end()); // Dump the Dataset on file d_segmentation.Dump(dateset_root_folder_segmentation / path(dateset_root_folder_segmentation.stem().string() + ".yml")); diff --git a/examples/example_ecvl_eddl.cpp b/examples/example_ecvl_eddl.cpp index c6f4a11d..e2cda679 100644 --- a/examples/example_ecvl_eddl.cpp +++ b/examples/example_ecvl_eddl.cpp @@ -13,7 +13,6 @@ #include #include -#include #include "ecvl/core.h" #include "ecvl/support_eddl.h" @@ -34,6 +33,7 @@ int main() // Create an augmentation sequence to be applied to the image auto augs = make_shared( + AugCenterCrop(), // Make image squared AugRotate({ -5, 5 }), AugMirror(.5), AugFlip(.5), @@ -71,6 +71,7 @@ int main() cout << "Executing TensorToView" << endl; TensorToView(t, view); + // Create an augmentation sequence from stream stringstream ss( "SequentialAugmentationContainer\n" " AugRotate angle=[-5,5] center=(0,0) interp=\"linear\"\n" @@ -85,21 +86,28 @@ int main() auto newdeal_augs = AugmentationFactory::create(ss); newdeal_augs->Apply(tmp); + /*--------------------------------------------------------------------------------------------*/ + // Create the augmentations to be applied to the dataset images during training and test. - // nullptr is given as augmentation for validation because this split doesn't exist in the mnist dataset. auto training_augs = make_shared( AugRotate({ -5, 5 }), AugAdditiveLaplaceNoise({ 0, 0.2 * 255 }), AugCoarseDropout({ 0, 0.55 }, { 0.02,0.1 }, 0), AugAdditivePoissonNoise({ 0, 40 }), - AugResizeDim({ 30, 30 }) + AugResizeDim({ 30, 30 }), + AugToFloat32(255), + AugNormalize({ 0.449 }, { 0.226 }) // mean of imagenet stats ); auto test_augs = make_shared( - AugResizeDim({ 30, 30 }) + AugResizeDim({ 30, 30 }), + AugToFloat32(255), + AugNormalize({ 0.449 }, { 0.226 }) // mean of imagenet stats ); - DatasetAugmentations dataset_augmentations{ {training_augs, nullptr, test_augs } }; + // OLD version: now the number of augmentations must match the number of splits in the yml file + // DatasetAugmentations dataset_augmentations{ {training_augs, nullptr, test_augs } }; + DatasetAugmentations dataset_augmentations{ {training_augs, test_augs } }; int batch_size = 64; cout << "Creating a DLDataset" << endl; @@ -128,6 +136,10 @@ int main() d.SetSplit(SplitType::test); d.LoadBatch(x, y); + // Save some input images + ImWrite("mnist_batch.png", MakeGrid(x, 8, false)); + ImWrite("mnist_batch_normalized.png", MakeGrid(x, 8, true)); + delete x; delete y; delete t; diff --git a/examples/example_pipeline.cpp b/examples/example_pipeline.cpp new file mode 100644 index 00000000..7cdfebe7 --- /dev/null +++ b/examples/example_pipeline.cpp @@ -0,0 +1,138 @@ +/* +* ECVL - European Computer Vision Library +* Version: 0.3.4 +* copyright (c) 2021, Università degli Studi di Modena e Reggio Emilia (UNIMORE), AImageLab +* Authors: +* Costantino Grana (costantino.grana@unimore.it) +* Federico Bolelli (federico.bolelli@unimore.it) +* Michele Cancilla (michele.cancilla@unimore.it) +* Laura Canalini (laura.canalini@unimore.it) +* Stefano Allegretti (stefano.allegretti@unimore.it) +* All rights reserved. +*/ + +#include +#include +#include +#include + +#include "ecvl/augmentations.h" +#include "ecvl/core.h" +#include "ecvl/support_eddl.h" +#include "ecvl/core/filesystem.h" + +using namespace ecvl; +using namespace ecvl::filesystem; +using namespace eddl; +using namespace std; + +int main() +{ + // Create the augmentations to be applied to the dataset images during training and test. + auto training_augs = make_shared( + AugRotate({ -5, 5 }), + AugAdditiveLaplaceNoise({ 0, 0.2 * 255 }), + AugCoarseDropout({ 0, 0.55 }, { 0.02,0.1 }, 0), + AugAdditivePoissonNoise({ 0, 40 }), + AugToFloat32(255) + ); + + auto test_augs = make_shared(AugToFloat32(255)); + + // Replace the random seed with a fixed one to have reproducible experiments + AugmentationParam::SetSeed(0); + + DatasetAugmentations dataset_augmentations{ { training_augs, test_augs } }; + + constexpr int epochs = 5; + constexpr int batch_size = 200; + constexpr int num_workers = 4; + constexpr int queue_ratio = 5; + cout << "Creating a DLDataset" << endl; + + // Initialize the DLDataset + DLDataset d("../examples/data/mnist/mnist_reduced.yml", batch_size, dataset_augmentations, ColorType::GRAY, ColorType::none, num_workers, queue_ratio, { true, false }); + //DLDataset d("D:/Data/isic_skin_lesion/isic_skin_lesion/isic_classification.yml", batch_size, dataset_augmentations, ColorType::RGB, ColorType::none, num_workers, queue_ratio); + + ofstream of; + cv::TickMeter tm; + cv::TickMeter tm_epoch; + auto num_batches_training = d.GetNumBatches(SplitType::training); + auto num_batches_test = d.GetNumBatches(SplitType::test); + + for (int i = 0; i < epochs; ++i) { + tm_epoch.reset(); + tm_epoch.start(); + /* Resize to batch_size if we have done a resize previously + if (d.split_[d.current_split_].last_batch_ != batch_size){ + net->resize(batch_size); + } + */ + cout << "Starting training" << endl; + d.SetSplit(SplitType::training); + + // Reset current split with shuffling + d.ResetBatch(d.current_split_, true); + + // Spawn num_workers threads + d.Start(); + for (int j = 0; j < num_batches_training; ++j) { + tm.reset(); + tm.start(); + cout << "Epoch " << i << "/" << epochs - 1 << " (batch " << j << "/" << num_batches_training - 1 << ") - "; + cout << "|fifo| " << d.GetQueueSize() << " - "; + + // tuple, unique_ptr, unique_ptr> samples_and_labels; + // samples_and_labels = d.GetBatch(); + // or... + auto [samples, x, y] = d.GetBatch(); + + // Sleep in order to simulate EDDL train_batch + cout << "sleeping..."; + this_thread::sleep_for(chrono::milliseconds(500)); + // eddl::train_batch(net, { x.get() }, { y.get() }); + + tm.stop(); + cout << "Elapsed time: " << tm.getTimeMilli() << endl; + } + d.Stop(); + + cout << "Starting test" << endl; + d.SetSplit(SplitType::test); + + // Reset current split without shuffling + d.ResetBatch(d.current_split_, false); + + d.Start(); + for (int j = 0; j < num_batches_test; ++j) { + tm.reset(); + tm.start(); + cout << "Test: Epoch " << i << "/" << epochs - 1 << " (batch " << j << "/" << num_batches_test - 1 << ") - "; + cout << "|fifo| " << d.GetQueueSize() << " - "; + + // tuple, unique_ptr, unique_ptr> samples_and_labels; + // samples_and_labels = d.GetBatch(); + // or... + auto [_, x, y] = d.GetBatch(); + + /* Resize net for last batch + if (auto x_batch = x->shape[0]; j == num_batches_test - 1 && x_batch != batch_size) { + // last mini-batch could have different size + net->resize(x_batch); + } + */ + // Sleep in order to simulate EDDL evaluate_batch + cout << "sleeping... - "; + this_thread::sleep_for(chrono::milliseconds(500)); + // eddl::eval_batch(net, { x.get() }, { y.get() }); + + tm.stop(); + cout << "Elapsed time: " << tm.getTimeMilli() << endl; + } + d.Stop(); + tm_epoch.stop(); + cout << "Epoch elapsed time: " << tm_epoch.getTimeSec() << endl; + } + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/modules/core/include/ecvl/CMakeLists.txt b/modules/core/include/ecvl/CMakeLists.txt index f513e3c0..9ec39a96 100644 --- a/modules/core/include/ecvl/CMakeLists.txt +++ b/modules/core/include/ecvl/CMakeLists.txt @@ -13,6 +13,7 @@ target_sources(ECVL_CORE PRIVATE core.h + core/any.h core/arithmetic.h core/arithmetic_impl.inc.h core/cpu_hal.h diff --git a/modules/core/include/ecvl/core/any.h b/modules/core/include/ecvl/core/any.h new file mode 100644 index 00000000..6d00771a --- /dev/null +++ b/modules/core/include/ecvl/core/any.h @@ -0,0 +1,93 @@ +/* +* ECVL - European Computer Vision Library +* Version: 0.3.4 +* copyright (c) 2021, Università degli Studi di Modena e Reggio Emilia (UNIMORE), AImageLab +* Authors: +* Costantino Grana (costantino.grana@unimore.it) +* Federico Bolelli (federico.bolelli@unimore.it) +* Michele Cancilla (michele.cancilla@unimore.it) +* Laura Canalini (laura.canalini@unimore.it) +* Stefano Allegretti (stefano.allegretti@unimore.it) +* All rights reserved. +*/ + +// We haven't checked which any to include yet +#ifndef INCLUDE_STD_ANY_EXPERIMENTAL + +// Check for feature test macro for +# if defined(__cpp_lib_any) +# define INCLUDE_STD_ANY_EXPERIMENTAL 0 + +// Check for feature test macro for +# elif defined(__cpp_lib_experimental_any) +# define INCLUDE_STD_ANY_EXPERIMENTAL 1 + +// We can't check if headers exist... +// Let's assume experimental to be safe +# elif !defined(__has_include) +# define INCLUDE_STD_ANY_EXPERIMENTAL 1 + +// Check if the header "" exists +# elif __has_include() + +// If we're compiling on Visual Studio and are not compiling with C++17, we need to use experimental +# ifdef _MSC_VER + +// Check and include header that defines "_HAS_CXX17" +# if __has_include() +# include + +// Check for enabled C++17 support +# if defined(_HAS_CXX17) && _HAS_CXX17 +// We're using C++17, so let's use the normal version +# define INCLUDE_STD_ANY_EXPERIMENTAL 0 +# endif +# endif + +// If the macro isn't defined yet, that means any of the other VS specific checks failed, so we need to use experimental +# ifndef INCLUDE_STD_ANY_EXPERIMENTAL +# define INCLUDE_STD_ANY_EXPERIMENTAL 1 +# endif + +// Not on Visual Studio. Let's use the normal version +# else // #ifdef _MSC_VER +# define INCLUDE_STD_ANY_EXPERIMENTAL 0 +# endif + +// Check if the header "" exists +# elif __has_include() +# define INCLUDE_STD_ANY_EXPERIMENTAL 1 + +// Fail if neither header is available with a nice error message +# else +# error Could not find system header "" or "" +# endif + +// We previously determined that we need the experimental version +# if INCLUDE_STD_ANY_EXPERIMENTAL +# include +namespace ecvl +{ +using any = std::experimental::any; + +template +auto any_cast(const T& t) +{ + return std::experimental::any_cast(t); +} +} +# else +# include +namespace ecvl +{ +using any = std::any; + +template +auto any_cast(const T& t) +{ + return std::any_cast(t); +} +} +# endif + +#endif // #ifndef INCLUDE_STD_ANY_EXPERIMENTAL \ No newline at end of file diff --git a/modules/core/include/ecvl/core/imgcodecs.h b/modules/core/include/ecvl/core/imgcodecs.h index 68f4a19b..2768b82e 100644 --- a/modules/core/include/ecvl/core/imgcodecs.h +++ b/modules/core/include/ecvl/core/imgcodecs.h @@ -27,7 +27,7 @@ namespace ecvl */ enum class ImReadMode { - //IMREAD_UNCHANGED = -1, //!< If set, return the loaded image as is (with alpha channel, otherwise it gets cropped). + UNCHANGED = -1, //!< If set, return the loaded image as is (with alpha channel, otherwise it gets cropped). GRAYSCALE = 0, //!< If set, always convert image to the single channel grayscale image (codec internal conversion). COLOR = 1, //!< If set, always convert image to the 3 channel BGR color image. //IMREAD_ANYDEPTH = 2, //!< If set, return 16-bit/32-bit image when the input has the corresponding depth, otherwise convert it to 8-bit. @@ -43,12 +43,39 @@ be read for any reason, the function creates an empty Image and returns false. @param[in] filename A std::filesystem::path identifying the file name. @param[out] dst Image in which data will be stored. -@param[in] flags An ImReadMode indicating how to read the image. +@param[in] flags \ref ImReadMode indicating how to read the image. @return true if the image is correctly read, false otherwise. */ bool ImRead(const ecvl::filesystem::path& filename, Image& dst, ImReadMode flags = ImReadMode::ANYCOLOR); +/** +@brief Loads an image from a buffer in memory. This is an overloaded function, provided for convenience. + +The buffer must be a raw encoded image (png, jpg). +If the image cannot be read for any reason, the function creates an empty Image and returns false. + +@param[in] buffer A char* identifying the input buffer. +@param[in] size Dimension of the input buffer. +@param[out] dst Image in which data will be stored. +@param[in] flags \ref ImReadMode indicating how to read the image. + +@return true if the image is correctly read, false otherwise. +*/ +bool ImRead(const char* buffer, const int size, Image& dst, ImReadMode flags = ImReadMode::ANYCOLOR); + +/** @brief Loads an image from a buffer in memory. This is an overloaded function, provided for convenience. + +It differs from the above function only because it accepts a std::vector instead of a char*. + +@param[in] buffer A std::vector identifying the input buffer. +@param[out] dst Image in which data will be stored. +@param[in] flags \ref ImReadMode indicating how to read the image. + +@return true if the image is correctly read, false otherwise. +*/ +bool ImRead(const std::vector& buffer, Image& dst, ImReadMode flags = ImReadMode::ANYCOLOR); + /** @brief Loads a multi-page image from a file. The function ImReadMulti loads a multi-page image from the specified file. If the image cannot diff --git a/modules/core/include/ecvl/core/support_dcmtk.h b/modules/core/include/ecvl/core/support_dcmtk.h index 118fe2a6..f1b6b0e4 100644 --- a/modules/core/include/ecvl/core/support_dcmtk.h +++ b/modules/core/include/ecvl/core/support_dcmtk.h @@ -57,6 +57,12 @@ The function DicomWrite saves the input image into a specified file, with the DI */ extern bool DicomWrite(const ecvl::filesystem::path& filename, const Image& src); +struct InitDCMTK +{ + InitDCMTK(); + ~InitDCMTK(); +}; + /** @example example_nifti_dicom.cpp Nifti and Dicom support example. */ diff --git a/modules/core/src/imgcodecs.cpp b/modules/core/src/imgcodecs.cpp index 4d8f7c2a..5d1e92f7 100644 --- a/modules/core/src/imgcodecs.cpp +++ b/modules/core/src/imgcodecs.cpp @@ -52,6 +52,21 @@ bool ImRead(const path& filename, Image& dst, ImReadMode flags) } } +bool ImRead(const std::vector& buffer, Image& dst, ImReadMode flags) +{ + cv::InputArray ia(buffer); + dst = MatToImage(cv::imdecode(ia, (int)flags)); + + // TODO: Nifti and Dicom version? + return !dst.IsEmpty(); +} + +bool ImRead(const char* buffer, const int size, Image& dst, ImReadMode flags) +{ + const std::vector buf(buffer, buffer + size); + return ImRead(buf, dst, flags); +} + bool ImReadMulti(const path& filename, Image& dst) { std::vector v; diff --git a/modules/core/src/support_dcmtk.cpp b/modules/core/src/support_dcmtk.cpp index 6e4fed7d..3e718de8 100644 --- a/modules/core/src/support_dcmtk.cpp +++ b/modules/core/src/support_dcmtk.cpp @@ -31,6 +31,16 @@ using namespace std; namespace ecvl { + +InitDCMTK::InitDCMTK() +{ + DJDecoderRegistration::registerCodecs(); +} +InitDCMTK::~InitDCMTK() +{ + DJDecoderRegistration::cleanup(); +} + bool OverlayMetaData::Query(const std::string& name, std::string& value) const { if (name == "overlay") { @@ -50,10 +60,9 @@ bool OverlayMetaData::Query(const std::string& name, std::string& value) const bool DicomRead(const std::string& filename, Image& dst) { + static InitDCMTK init_dcmtk; // Created only first time DicomRead is called bool return_value = true; - DJDecoderRegistration::registerCodecs(); - DicomImage* image = new DicomImage(filename.c_str()); if (image == NULL) { return_value = false; @@ -93,7 +102,7 @@ bool DicomRead(const std::string& filename, Image& dst) } else { for (int i = 0; i < planes; i++) { - memcpy(dst.data_ + x * y * DataTypeSize(dst_datatype) * i, reinterpret_cast(dipixel_data)[i], x * y * DataTypeSize(dst_datatype)); + memcpy(dst.data_ + x * y * DataTypeSize(dst_datatype) * i, reinterpret_cast(dipixel_data)[i], x * y * DataTypeSize(dst_datatype)); } } @@ -158,8 +167,6 @@ bool DicomRead(const std::string& filename, Image& dst) dst = Image(); } - DJDecoderRegistration::cleanup(); - return return_value; } diff --git a/modules/dataset/include/ecvl/dataset_generator.h b/modules/dataset/include/ecvl/dataset_generator.h index bef52168..72e85166 100644 --- a/modules/dataset/include/ecvl/dataset_generator.h +++ b/modules/dataset/include/ecvl/dataset_generator.h @@ -39,11 +39,8 @@ class GenerateDataset dataset_root_directory_(dataset_root_directory) { for (auto& p : filesystem::directory_iterator(dataset_root_directory_)) { - std::string tmp = p.path().stem().string(); - - // Check if split folders exist - if (tmp == "training" || tmp == "validation" || tmp == "test") { - splits_.emplace_back(tmp); + if (filesystem::is_directory(p)) { + splits_.emplace_back(p.path().stem().string()); } } num_samples_.resize(splits_.size()); diff --git a/modules/dataset/include/ecvl/dataset_parser.h b/modules/dataset/include/ecvl/dataset_parser.h index 770b333e..32c8b623 100644 --- a/modules/dataset/include/ecvl/dataset_parser.h +++ b/modules/dataset/include/ecvl/dataset_parser.h @@ -15,10 +15,12 @@ #define ECVL_DATASET_PARSER_H_ #include "ecvl/core.h" +#include "ecvl/core/any.h" #include "ecvl/core/filesystem.h" #include "ecvl/core/optional.h" #include +#include #include #include #include @@ -36,6 +38,17 @@ namespace ecvl @anchor SplitType */ UNSIGNED_ENUM_CLASS(SplitType, training, validation, test) + +/** @brief Enum class representing allowed tasks for the ECVL Dataset. + +@anchor Task + */ +enum class Task +{ + classification, + segmentation, +}; + /** @brief Sample image in a dataset. This class provides the information to describe a dataset sample. @@ -45,11 +58,11 @@ This class provides the information to describe a dataset sample. class Sample { public: - std::vector location_; /**< @brief Absolute path of the sample. */ - optional> label_; /**< @brief Vector of sample labels. */ - optional label_path_; /**< @brief Absolute path of sample ground truth. */ - optional> values_; /**< @brief Map (`map`) which stores the features of a sample. */ - std::vector size_; /**< @brief Original x and y dimensions of the sample */ + std::vector location_; /**< @brief Absolute path of the sample. */ + optional> label_; /**< @brief Vector of sample labels. */ + optional label_path_; /**< @brief Absolute path of sample ground truth. */ + optional> values_; /**< @brief Map (`map`) which stores the features of a sample. */ + std::vector size_; /**< @brief Original x and y dimensions of the sample */ /** @brief Return an Image of the dataset. @@ -60,21 +73,50 @@ class Sample @return Image containing the loaded sample. */ - ecvl::Image LoadImage(ecvl::ColorType ctype = ecvl::ColorType::BGR, const bool& is_gt = false); + ecvl::Image LoadImage(ecvl::ColorType ctype = ecvl::ColorType::RGB, const bool& is_gt = false); }; -/** @brief Splits of a dataset. - -This class provides the splits a dataset can have: training, validation, and test. - +/** @brief Split of a dataset. +This class provides the name of the split and the indices of the samples that belong to this split. +It optionally provides the split type if the split name is one of training, validation or test. @anchor Split */ class Split { public: - std::vector training_; /**< @brief Vector containing samples of training split. */ - std::vector validation_; /**< @brief Vector containing samples of validation split. */ - std::vector test_; /**< @brief Vector containing samples of test split. */ + std::string split_name_; /**< @brief Name of the split. */ + optional split_type_; /**< @brief If the split is training, validation or test the corresponding SpitType is provided. */ + std::vector samples_indices_; /**< @brief Vector containing samples indices of the split. */ + bool drop_last_ = false; /**< @brief Whether to drop elements that don't fit batch size or not. */ + int num_batches_; /**< @brief Number of batches of this split. */ + int last_batch_; /**< @brief Dimension of the last batch of this split. */ + bool no_label_ = false; /**< @brief Whether the split has samples with labels or not. */ + + Split() {} + + /** + @param[in] split_name Name of the split. + @param[in] samples_indices Vector containing samples indices of the split. + */ + Split(const std::string& split_name, const std::vector& samples_indices) : split_name_{ split_name }, samples_indices_{ samples_indices } + { + if (split_name_ == "training") split_type_ = SplitType::training; + else if (split_name_ == "validation") split_type_ = SplitType::validation; + else if (split_name_ == "test") split_type_ = SplitType::test; + } + + void SetNumBatches(int batch_size) + { + num_batches_ = drop_last_ ? vsize(samples_indices_) / batch_size : (vsize(samples_indices_) + batch_size - 1) / batch_size; + } + + void SetLastBatch(int batch_size) + { + // last batch is the remainder of the number of samples of the split divided by the batch size. + // if drop last is true or the remainder is 0, last batch is equal to the batch size. + auto value = vsize(samples_indices_) % batch_size; + last_batch_ = drop_last_ ? batch_size : (value == 0 ? batch_size : value); + } }; /** @brief DeepHealth Dataset. @@ -85,13 +127,21 @@ This class implements the DeepHealth Dataset Format (https://github.com/deepheal */ class Dataset { + std::map features_map_; + void DecodeImages(const YAML::Node& node, const filesystem::path& root_path, bool verify); + void FindLabel(Sample& sample, const YAML::Node& n); +protected: + std::vector::iterator GetSplitIt(ecvl::any split); + const int GetSplitIndex(ecvl::any split); public: - std::string name_ = "DeepHealth dataset"; /**< @brief Name of the Dataset. */ - std::string description_ = "This is the DeepHealth example dataset!"; /**< @brief Description of the Dataset. */ - std::vector classes_; /**< @brief Vector with all the classes available in the Dataset. */ - std::vector features_; /**< @brief Vector with all the features available in the Dataset. */ - std::vector samples_; /**< @brief Vector containing all the Dataset samples. See @ref Sample. */ - Split split_; /**< @brief Splits of the Dataset. See @ref Split. */ + std::string name_ = "DeepHealth dataset"; /**< @brief Name of the Dataset. */ + std::string description_ = "This is the DeepHealth example dataset!"; /**< @brief Description of the Dataset. */ + std::vector classes_; /**< @brief Vector with all the classes available in the Dataset. */ + std::vector features_; /**< @brief Vector with all the features available in the Dataset. */ + std::vector samples_; /**< @brief Vector containing all the Dataset samples. See @ref Sample. */ + std::vector split_; /**< @brief Splits of the Dataset. See @ref Split. */ + int current_split_ = -1; /**< @brief Current split from which images are loaded. */ + Task task_; /**< @brief Task of the dataset. */ Dataset() {} @@ -101,6 +151,22 @@ class Dataset */ Dataset(const filesystem::path& filename, bool verify = false); + /* Destructor */ + virtual ~Dataset() {} + + /** @brief Returns the image indexes of the requested split. + + If no split is provided or an illegal value is provided, the current split is returned. + @param[in] split index, name or ecvl::SplitType representing the split to get. + @return vector of image indexes of the requested split. + */ + std::vector& GetSplit(const ecvl::any& split = -1); + + /** @brief Set the current split. + @param[in] split index, name or ecvl::SplitType representing the split to set. + */ + void SetSplit(const ecvl::any& split); + /** @brief Dump the Dataset into a YAML file following the DeepHealth Dataset Format. The YAML file is saved into the dataset root directory. @@ -110,48 +176,17 @@ class Dataset */ void Dump(const filesystem::path& file_path); - // RegEx which matchs URLs - static const std::regex url_regex_; + /** @brief Retrieve the list of all samples locations in the dataset file. -private: - std::map features_map_; - void DecodeImages(const YAML::Node& node, const filesystem::path& root_path, bool verify); - void FindLabel(Sample& sample, const YAML::Node& n); -}; -} // namespace ecvl + A single Sample can have multiple locations (e.g., if they are different acquisitions of the same image). -/** @cond HIDDEN_SECTION */ -namespace YAML -{ -/** - Enable YAML decoding of Split. - Hidden from docs. -*/ -template<> -struct convert -{ - /*static Node encode(const ecvl::Split& rhs) - { - Node node; - node.push_back(rhs.x); - return node; - }*/ + @return vector containing all the samples locations. + */ + std::vector> GetLocations() const; - static bool decode(const YAML::Node& node, ecvl::Split& rhs) - { - if (node["training"].IsDefined()) { - rhs.training_ = node["training"].as>(); - } - if (node["validation"].IsDefined()) { - rhs.validation_ = node["validation"].as>(); - } - if (node["test"].IsDefined()) { - rhs.test_ = node["test"].as>(); - } - return true; - } + // RegEx which matchs URLs + static const std::regex url_regex_; }; -} // namespace YAML -/** @endcond */ +} // namespace ecvl #endif // ECVL_DATASET_PARSER_H_ \ No newline at end of file diff --git a/modules/dataset/src/dataset_generator.cpp b/modules/dataset/src/dataset_generator.cpp index 764897a8..e0d9ecd9 100644 --- a/modules/dataset/src/dataset_generator.cpp +++ b/modules/dataset/src/dataset_generator.cpp @@ -144,19 +144,11 @@ void GenerateDataset::LoadImagesAndSplits() } // load indexes of images for each split + d_.split_.resize(splits_.size()); for (int i = 0; i < splits_.size(); ++i) { - if (splits_[i] == "training") { - d_.split_.training_.resize(num_samples_[i]); - iota(d_.split_.training_.begin(), d_.split_.training_.end(), img_index); - } - else if (splits_[i] == "validation") { - d_.split_.validation_.resize(num_samples_[i]); - iota(d_.split_.validation_.begin(), d_.split_.validation_.end(), img_index); - } - else if (splits_[i] == "test") { - d_.split_.test_.resize(num_samples_[i]); - iota(d_.split_.test_.begin(), d_.split_.test_.end(), img_index); - } + d_.split_[i].split_name_ = splits_[i]; + d_.split_[i].samples_indices_.resize(num_samples_[i]); + iota(d_.split_[i].samples_indices_.begin(), d_.split_[i].samples_indices_.end(), img_index); img_index += num_samples_[i]; } } diff --git a/modules/dataset/src/dataset_parser.cpp b/modules/dataset/src/dataset_parser.cpp index 49035187..5cd6c730 100644 --- a/modules/dataset/src/dataset_parser.cpp +++ b/modules/dataset/src/dataset_parser.cpp @@ -13,6 +13,7 @@ #include "ecvl/dataset_parser.h" +#include #include #include @@ -225,26 +226,13 @@ void Dataset::Dump(const path& file_path) } } - if (split_.training_.size() > 0 || split_.validation_.size() > 0 || split_.test_.size() > 0) { + if (split_.size() > 0) { os << "split:" << endl; - } - - if (split_.training_.size() > 0) { - os << tab + "training:" << endl; - for (auto& i : split_.training_) { - os << tab + tab + "- " << i << endl; - } - } - if (split_.validation_.size() > 0) { - os << tab + "validation:" << endl; - for (auto& i : split_.validation_) { - os << tab + tab + "- " << i << endl; - } - } - if (split_.test_.size() > 0) { - os << tab + "test:" << endl; - for (auto& i : split_.test_) { - os << tab + tab + "- " << i << endl; + for (auto& s : split_) { + os << tab + s.split_name_ + ":" << endl; + for (auto& i : s.samples_indices_) { + os << tab + tab + "- " << i << endl; + } } } @@ -287,8 +275,92 @@ Dataset::Dataset(const filesystem::path& filename, bool verify) } DecodeImages(config["images"], abs_filename.parent_path(), verify); + if (config["split"].IsDefined()) { - this->split_ = config["split"].as(); + for (YAML::const_iterator it = config["split"].begin(); it != config["split"].end(); ++it) { + // insert into the vector split_ the split name and the vector of image indices + Split s(it->first.as(), it->second.as>()); + + if (!samples_[s.samples_indices_[0]].label_ && !samples_[s.samples_indices_[0]].label_path_) { + s.no_label_ = true; + } + split_.push_back(s); + } + } + + task_ = classes_.empty() ? Task::segmentation : Task::classification; +} + +const int Dataset::GetSplitIndex(any split) +{ + if (split.type() == typeid(int)) { + auto s = any_cast(split); + int index = s < 0 || s >= split_.size() ? current_split_ : s; + return index; + } + else { + return static_cast(distance(split_.begin(), GetSplitIt(split))); + } +} + +vector::iterator Dataset::GetSplitIt(any split) +{ + if (split.type() == typeid(int)) { + try { + auto s = any_cast(split); + const int index = s < 0 || s >= split_.size() ? current_split_ : s; + return split_.begin() + index; + } + catch (const out_of_range) { + ECVL_ERROR_SPLIT_DOES_NOT_EXIST + } + } + auto func = [&](const auto& s) { + if (split.type() == typeid(string)) { + auto tmp = s.split_name_; + return tmp == any_cast(split); + } + else if (split.type() == typeid(const char*)) { + auto tmp = s.split_name_; + return tmp == any_cast(split); + } + else if (split.type() == typeid(SplitType)) { + auto tmp = s.split_type_; + return tmp == any_cast(split); + } + else { + ECVL_ERROR_SPLIT_DOES_NOT_EXIST + } + }; + + auto it = std::find_if(split_.begin(), split_.end(), [&](const auto& s) { return func(s); }); + if (it == this->split_.end()) { + ECVL_ERROR_SPLIT_DOES_NOT_EXIST + } + else { + return it; + } +} + +vector& Dataset::GetSplit(const any& split) +{ + auto it = GetSplitIt(split); + return it->samples_indices_; +} + +void Dataset::SetSplit(const any& split) +{ + int index = GetSplitIndex(split); + this->current_split_ = index; +} + +vector> Dataset::GetLocations() const +{ + const auto& size = vsize(samples_); + vector> locations(size); + for (int i = 0; i < size; ++i) { + locations[i] = samples_[i].location_; } + return locations; } } \ No newline at end of file diff --git a/modules/dataset/test/test_dataset_parser.cpp b/modules/dataset/test/test_dataset_parser.cpp index 5fab2e2e..f40af7f6 100644 --- a/modules/dataset/test/test_dataset_parser.cpp +++ b/modules/dataset/test/test_dataset_parser.cpp @@ -26,11 +26,11 @@ using namespace ecvl; #ifdef ECVL_WITH_EXAMPLES TEST(DatasetParser, LoadExistingDataset) { - Dataset d(CMAKE_CURRENT_SOURCE_DIR "/examples/data/mnist/mnist.yml"); + Dataset d(CMAKE_CURRENT_SOURCE_DIR "/examples/data/mnist/mnist_reduced.yml"); EXPECT_EQ(d.name_, "MNIST"); EXPECT_EQ(d.classes_.size(), 10); EXPECT_THAT(d.classes_, testing::ElementsAre("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")); - EXPECT_EQ(d.samples_.size(), 70000); + EXPECT_EQ(d.samples_.size(), 1000); } #endif diff --git a/modules/eddl/include/ecvl/augmentations.h b/modules/eddl/include/ecvl/augmentations.h index 7b9f0960..ad64e9a9 100644 --- a/modules/eddl/include/ecvl/augmentations.h +++ b/modules/eddl/include/ecvl/augmentations.h @@ -219,11 +219,13 @@ class Augmentation } RealApply(img, gt); } + virtual std::shared_ptr Clone() const = 0; virtual ~Augmentation() = default; private: virtual void RealApply(ecvl::Image& img, const ecvl::Image& gt = Image()) = 0; }; +#define DEFINE_AUGMENTATION_CLONE(class_name) std::shared_ptr Clone() const override { return std::make_shared(*this); } struct AugmentationFactory { @@ -261,11 +263,20 @@ class SequentialAugmentationContainer : public Augmentation } std::vector> augs_; /**< @brief vector containing the Augmentation to be applied */ public: + DEFINE_AUGMENTATION_CLONE(SequentialAugmentationContainer) + template SequentialAugmentationContainer(Ts&&... t) : augs_({ std::make_shared(std::forward(t))... }) {} SequentialAugmentationContainer(std::vector> augs) : augs_(augs) {} + SequentialAugmentationContainer(const SequentialAugmentationContainer& other) : Augmentation(other) + { + for (const auto& a : other.augs_) { + augs_.emplace_back(a->Clone()); + } + } + SequentialAugmentationContainer(std::istream& is) { while (true) { @@ -306,6 +317,8 @@ class OneOfAugmentationContainer : public Augmentation std::vector> augs_; /**< @brief vector containing the Augmentation to be applied */ double p_; public: + DEFINE_AUGMENTATION_CLONE(OneOfAugmentationContainer) + template OneOfAugmentationContainer(double p, Ts&&... t) : p_(p), augs_({ std::make_shared(std::forward(t))... }) { @@ -317,6 +330,13 @@ class OneOfAugmentationContainer : public Augmentation params_["p"] = AugmentationParam(0, 1); } + OneOfAugmentationContainer(const OneOfAugmentationContainer& other) : Augmentation(other) + { + for (const auto& a : other.augs_) { + augs_.emplace_back(a->Clone()); + } + } + OneOfAugmentationContainer(std::istream& is) { param p; @@ -369,6 +389,8 @@ class AugRotate : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugRotate) + /** @brief AugRotate constructor @param[in] angle Parameter which determines the range of degrees [min,max] to randomly select from. @@ -434,6 +456,8 @@ class AugResizeDim : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugResizeDim) + /** @brief AugResizeDim constructor @param[in] dims std::vector that specifies the new size of each dimension. @@ -484,6 +508,8 @@ class AugResizeScale : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugResizeScale) + /** @brief AugResizeScale constructor @param[in] scale std::vector that specifies the scale to apply to each dimension. @@ -534,6 +560,8 @@ class AugFlip : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugFlip) + /** @brief AugFlip constructor @param[in] p Probability of each image to get flipped. @@ -573,6 +601,8 @@ class AugMirror : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugMirror) + /** @brief AugMirror constructor @param[in] p Probability of each image to get mirrored. @@ -605,6 +635,8 @@ class AugGaussianBlur : public Augmentation GaussianBlur(img, img, sigma); } public: + DEFINE_AUGMENTATION_CLONE(AugGaussianBlur) + /** @brief AugGaussianBlur constructor @param[in] sigma Parameter which determines the range of sigma [min,max] to randomly select from. @@ -636,6 +668,8 @@ class AugAdditiveLaplaceNoise : public Augmentation AdditiveLaplaceNoise(img, img, std_dev); } public: + DEFINE_AUGMENTATION_CLONE(AugAdditiveLaplaceNoise) + /** @brief AugAdditiveLaplaceNoise constructor @param[in] std_dev Parameter which determines the range of values [min,max] to randomly select the standard deviation of the noise generating distribution. @@ -668,6 +702,8 @@ class AugAdditivePoissonNoise : public Augmentation AdditivePoissonNoise(img, img, lambda); } public: + DEFINE_AUGMENTATION_CLONE(AugAdditivePoissonNoise) + /** @brief AugAdditivePoissonNoise constructor @param[in] lambda Parameter which determines the range of values [min,max] to randomly select the lambda of the noise generating distribution. @@ -700,6 +736,8 @@ class AugGammaContrast : public Augmentation GammaContrast(img, img, gamma); } public: + DEFINE_AUGMENTATION_CLONE(AugGammaContrast) + /** @brief AugGammaContrast constructor @param[in] gamma Parameter which determines the range of values [min,max] to randomly select the exponent for the contrast adjustment. @@ -736,6 +774,8 @@ class AugCoarseDropout : public Augmentation CoarseDropout(img, img, p, drop_size, per_channel); } public: + DEFINE_AUGMENTATION_CLONE(AugCoarseDropout) + /** @brief AugCoarseDropout constructor @param[in] p Parameter which determines the range of values [min,max] to randomly select the probability of any rectangle being set to zero. @@ -785,6 +825,8 @@ class AugTranspose : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugTranspose) + /** @brief AugTranspose constructor @param[in] p Probability of each image to get transposed. @@ -817,6 +859,8 @@ class AugBrightness : public Augmentation Add(img, beta, img); } public: + DEFINE_AUGMENTATION_CLONE(AugBrightness) + /** @brief AugBrightness constructor @param[in] beta Parameter which determines the range of values [min,max] to randomly select the value for the brightness adjustment. @@ -861,6 +905,8 @@ class AugGridDistortion : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugGridDistortion) + /** @brief AugGridDistortion constructor @param[in] num_steps Parameter which determines the range of values [min,max] to randomly select the number of grid cells on each side. @@ -947,6 +993,8 @@ class AugElasticTransform : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugElasticTransform) + /** @brief AugElasticTransform constructor @param[in] alpha Parameter which determines the range of values [min,max] to randomly select the scaling factor that controls the intensity of the deformation. @@ -1035,6 +1083,8 @@ class AugOpticalDistortion : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugOpticalDistortion) + /** @brief AugOpticalDistortion constructor @param[in] distort_limit Parameter which determines the range of values [min,max] to randomly select the distortion steps. @@ -1114,6 +1164,8 @@ class AugSalt : public Augmentation Salt(img, img, p, per_channel, static_cast(seed)); } public: + DEFINE_AUGMENTATION_CLONE(AugSalt) + /** @brief AugSalt constructor @param[in] p Parameter which determines the range of values [min,max] to randomly select the probability of any pixel being set to white. @@ -1154,11 +1206,13 @@ class AugPepper : public Augmentation virtual void RealApply(ecvl::Image& img, const ecvl::Image& gt = Image()) override { const auto p = params_["p"].value_; - const auto seed = params_["seed"].value_; + const auto seed = params_["seed"].value_; const bool per_channel = params_["per_channel"].value_ <= per_channel_ ? true : false; Pepper(img, img, p, per_channel, static_cast(seed)); } public: + DEFINE_AUGMENTATION_CLONE(AugPepper) + /** @brief AugPepper constructor @param[in] p Parameter which determines the range of values [min,max] to randomly select the probability of any pixel being set to black. @@ -1199,11 +1253,13 @@ class AugSaltAndPepper : public Augmentation virtual void RealApply(ecvl::Image& img, const ecvl::Image& gt = Image()) override { const auto p = params_["p"].value_; - const auto seed = params_["seed"].value_; + const auto seed = params_["seed"].value_; const bool per_channel = params_["per_channel"].value_ <= per_channel_ ? true : false; SaltAndPepper(img, img, p, per_channel, static_cast(seed)); } public: + DEFINE_AUGMENTATION_CLONE(AugSaltAndPepper) + /** @brief AugSaltAndPepper constructor @param[in] p Parameter which determines the range of values [min,max] to randomly select the probability of any pixel being set to white or black. @@ -1255,6 +1311,8 @@ class AugNormalize : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugNormalize) + /** @brief AugNormalize constructor @param[in] mean Mean to substract from all pixel. @@ -1302,29 +1360,48 @@ class AugNormalize : public Augmentation class AugCenterCrop : public Augmentation { std::vector size_; + bool infer_; virtual void RealApply(ecvl::Image& img, const ecvl::Image& gt = Image()) override { - CenterCrop(img, img, size_); + std::vector new_size = size_; + if (infer_) { + // TODO: 3D implementation + new_size = std::vector(2, std::min(img.Width(), img.Height())); + } + CenterCrop(img, img, new_size); if (!gt.IsEmpty()) { - CenterCrop(gt, const_cast(gt), size_); + CenterCrop(gt, const_cast(gt), new_size); } } public: + DEFINE_AUGMENTATION_CLONE(AugCenterCrop) + + /** @brief AugCenterCrop constructor. Crop size is inferred from the minimum image dimension. + \f$ + crop\_size = min(Image_{cols}, Image_{rows}) + \f$ + */ + AugCenterCrop() : infer_{ true } {} + /** @brief AugCenterCrop constructor @param[in] size std::vector that specifies the new size of each dimension [w,h]. */ - AugCenterCrop(const std::vector& size) : size_{ size } {} + AugCenterCrop(const std::vector& size) : size_{ size }, infer_{ false } {} AugCenterCrop(std::istream& is) { auto m = param::read(is, "AugCenterCrop"); param p; - m.Get("size", param::type::vector, true, p); - for (const auto& x : p.vals_) { - size_.emplace_back(static_cast(x)); + if (m.Get("size", param::type::vector, false, p)) { + for (const auto& x : p.vals_) { + size_.emplace_back(static_cast(x)); + } + infer_ = false; + } else { + infer_ = true; } } }; @@ -1350,6 +1427,8 @@ class AugToFloat32 : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugToFloat32) + /** @brief AugToFloat32 constructor @param[in] divisor Value used to divide the img Image. @@ -1385,6 +1464,8 @@ class AugDivBy255 : public Augmentation } } public: + DEFINE_AUGMENTATION_CLONE(AugDivBy255) + /** @brief AugDivBy255 constructor */ AugDivBy255() {} AugDivBy255(std::istream& is) {} @@ -1403,6 +1484,8 @@ class AugScaleTo : public Augmentation ScaleTo(img, img, new_min_, new_max_); } public: + DEFINE_AUGMENTATION_CLONE(AugScaleTo) + /** @brief AugScaleTo constructor @param[in] new_min double which indicates the new minimum value. diff --git a/modules/eddl/include/ecvl/support_eddl.h b/modules/eddl/include/ecvl/support_eddl.h index 95a7a085..df70f36c 100644 --- a/modules/eddl/include/ecvl/support_eddl.h +++ b/modules/eddl/include/ecvl/support_eddl.h @@ -21,10 +21,66 @@ #include +#include #include +#include +#include namespace ecvl { +#define ECVL_ERROR_AUG_DOES_NOT_EXIST throw std::runtime_error(ECVL_ERROR_MSG "Augmentation for this split does not exist"); + +/** @brief Convert an ECVL Image into an EDDL Tensor. + +Image must have 3 dimensions "xy[czo]" (in any order). \n +Output Tensor will be created with shape \f$C\f$ x \f$H\f$ x \f$W\f$. \n + +@param[in] img Input ECVL Image. +@param[out] t Output EDDL Tensor. It is created inside the function. + +*/ +void ImageToTensor(const Image& img, Tensor*& t); + +/** @brief Insert an ECVL Image into an EDDL Tensor. + +This function is useful to insert into an EDDL Tensor more than one image, specifying how many images are already stored in the Tensor. +Image must have 3 dimensions "xy[czo]" (in any order). \n + +@param[in] img Input ECVL Image. +@param[out] t Output EDDL Tensor. It must be created with the right dimensions before calling this function. +@param[in] offset How many images are already stored in the Tensor. + +*/ +void ImageToTensor(const Image& img, Tensor*& t, const int& offset); + +/** @brief Convert an EDDL Tensor into an ECVL Image. + +Tensor dimensions must be \f$C\f$ x \f$H\f$ x \f$W\f$ or \f$N\f$ x \f$C\f$ x \f$H\f$ x \f$W\f$, where: \n +\f$N\f$ = batch size \n +\f$C\f$ = channels \n +\f$H\f$ = height \n +\f$W\f$ = width + +@param[in] t Input EDDL Tensor. +@param[out] img Output ECVL Image. It is a "xyo" with DataType::float32 and ColorType::none Image. + +*/ +void TensorToImage(const Tensor* t, Image& img); + +/** @brief Convert an EDDL Tensor into an ECVL View. + +Tensor dimensions must be \f$C\f$ x \f$H\f$ x \f$W\f$ or \f$N\f$ x \f$C\f$ x \f$H\f$ x \f$W\f$, where: \n +\f$N\f$ = batch size \n +\f$C\f$ = channels \n +\f$H\f$ = height \n +\f$W\f$ = width + +@param[in] t Input EDDL Tensor. +@param[out] v Output ECVL View. It is a "xyo" with ColorType::none View. + +*/ +void TensorToView(const Tensor* t, View& v); + /** @brief Dataset Augmentations. This class represent the augmentations which will be applied to each split. @@ -35,22 +91,230 @@ This is just a shallow container for the Augmentations */ class DatasetAugmentations { - std::array, 3> augs_; + std::vector> augs_; public: - DatasetAugmentations(std::array, 3> augs = { nullptr,nullptr,nullptr }) - : augs_(augs) - {} + DatasetAugmentations(const std::vector>& augs) : augs_(augs) {} + + // This makes a deep copy of the Augmentations + DatasetAugmentations(const DatasetAugmentations& other) + { + for (const auto& a : other.augs_) { + augs_.emplace_back(a ? a->Clone() : nullptr); + } + } + + // Getters: YAGNI -// Getters: YAGNI + bool Apply(const int split, Image& img, const Image& gt = Image()) + { + // check if the augs for split st are provided + try { + if (augs_.at(split)) { + augs_[split]->Apply(img, gt); + return true; + } + return false; + } + catch (const std::out_of_range) { + ECVL_ERROR_AUG_DOES_NOT_EXIST + } + } bool Apply(SplitType st, Image& img, const Image& gt = Image()) { - if (augs_[+st]) { // Magic + operator - augs_[+st]->Apply(img, gt); - return true; + return Apply(+st, img, gt); // Magic + operator + } + + bool IsEmpty() const + { + return augs_.empty(); + } +}; + +/** @brief Label class representing the Sample labels, which may have different representations depending on the task. + +@anchor Label +*/ +class Label +{ +public: + /** @brief Abstract function which copies the sample labels into the batch tensor. + + @param[in] tensor EDDL Tensor in which to copy the labels + @param[in] offset Position of the tensor from which to insert the sample labels + */ + virtual void ToTensorPlane(Tensor* tensor, int offset) = 0; + virtual ~Label() {}; +}; + +/** @brief Label for classification task. + +@anchor LabelClass +*/ +class LabelClass : public Label +{ +public: + vector label; /**< @brief Vector of the sample labels. */ + + /** @brief Convert the sample labels in a one-hot encoded tensor and copy it to the batch tensor. + + @param[in] tensor EDDL Tensor in which to copy the labels (dimensions: [batch_size, num_classes]) + @param[in] offset Position of the tensor from which to insert the sample labels + */ + void ToTensorPlane(Tensor* tensor, int offset) override + { + vector lab(tensor->shape[1], 0); + for (int j = 0; j < vsize(label); ++j) { + lab[label[j]] = 1; + } + //memcpy(tensor->ptr + lab.size() * offset, lab.data(), lab.size() * sizeof(float)); + std::copy(lab.data(), lab.data() + lab.size(), tensor->ptr + lab.size() * offset); + } +}; + +/** @brief Label for segmentation task. + +@anchor LabelImage +*/ +class LabelImage : public Label +{ +public: + Image gt; /**< @brief Ground truth image. */ + + /** @brief Convert the sample ground truth Image into a tensor and copy it to the batch tensor. + + @param[in] tensor EDDL Tensor in which to copy the ground truth (dimensions: [batch_size, num_channels, height, width]) + @param[in] offset Position of the tensor from which to insert the sample ground truth + */ + void ToTensorPlane(Tensor* tensor, int offset) override + { + ImageToTensor(gt, tensor, offset); + } +}; + +/** @brief Class that manages the producers-consumer queue of samples. +* The queue stores pairs of image and label, pushing and popping them in an exclusive way. +* The queue also has a maximum size (`max_size_` attribute) to avoid memory overflows. + +@anchor ProducersConsumerQueue +*/ +class ProducersConsumerQueue +{ + std::condition_variable cond_notempty_; /**< @brief Condition variable that wait if the queue is empty. */ + std::condition_variable cond_notfull_; /**< @brief Condition variable that wait if the queue is full. */ + std::mutex mutex_; /**< @brief Mutex to grant exclusive access to the queue. */ + std::queue> cpq_; /**< @brief Queue of samples, stored as tuple of Sample, Image and Label pointer. */ + unsigned max_size_; /**< @brief Maximum size of the queue. */ + unsigned threshold_; /**< @brief Threshold from which restart to produce samples. If not specified, it's set to the half of maximum size. */ + +public: + ProducersConsumerQueue() {} + /** + @param[in] mxsz Maximum size of the queue. + */ + ProducersConsumerQueue(unsigned mxsz) : max_size_(mxsz), threshold_(max_size_ / 2) {} + /** + @param[in] mxsz Maximum size of the queue. + @param[in] thresh Threshold from which restart to produce samples. + */ + ProducersConsumerQueue(unsigned mxsz, unsigned thresh) : max_size_(mxsz), threshold_(thresh) {} + + /** @brief Push a sample in the queue. + + Take the lock of the queue and wait if the queue is full. Otherwise, push the tuple Sample, Image, Label into the queue. + + @param[in] sample Sample to push in queue. + @param[in] image Image to push in the queue. + @param[in] label Label to push in the queue. + */ + void Push(const Sample& sample, const Image& image, Label* const label) + { + std::unique_lock lock(mutex_); + cond_notfull_.wait(lock, [this]() { return !IsFull(); }); + cpq_.push(make_tuple(sample, image, label)); + cond_notempty_.notify_one(); + } + + /** @brief Pop a sample from the queue. + + Take the lock of the queue and wait if the queue is empty. Otherwise, pop a Sample, Image and its Label from the queue. + If the queue size is still bigger than the half of the maximum size, don't notify the Push to avoid an always-full queue. + + @param[in] sample Sample to pop in queue. + @param[in] image Image to pop from the queue. + @param[in] label Label to pop from the queue. + */ + void Pop(Sample& sample, Image& image, Label*& label) + { + std::unique_lock lock(mutex_); + cond_notempty_.wait(lock, [this]() { return !IsEmpty(); }); + std::tie(sample, image, label) = cpq_.front(); + cpq_.pop(); + if (Length() < threshold_) { + cond_notfull_.notify_one(); } - return false; } + + /** @brief Check if the queue is full. + + @return true if the queue is full, false otherwise. + */ + bool IsFull() const + { + return cpq_.size() >= max_size_; + } + + /** @brief Check if the queue is empty. + + @return true if the queue is empty, false otherwise. + */ + bool IsEmpty() const + { + return cpq_.empty(); + } + + /** @brief Calculate the current size of the queue. + + @return the current size of the queue. + */ + size_t Length() const + { + return cpq_.size(); + } + + /** @brief Set the maximum size of the queue and optionally the threshold from which restart to produce samples. + + @param[in] max_size maximum size of the queue. + @param[in] thresh threshold from which restart to produce samples. If not specified, it's set to the half of maximum size. + */ + void SetSize(int max_size, int thresh = -1) + { + max_size_ = max_size; + threshold_ = thresh != -1 ? thresh : max_size / 2; + } + + void Clear() + { + std::unique_lock lock(mutex_); + cpq_ = {}; + } +}; + +/** @brief Class representing the thread counters. + +Each thread has its own indices to manage. The split samples have been assigned to several threads which manage them separately. + +@anchor ThreadCounters +*/ +class ThreadCounters +{ +public: + int counter_; /**< @brief Index of the sample currently used by the thread. */ + int min_, max_; /**< @brief Indices of samples managed by the thread in the interval [min_, max_). */ + + ThreadCounters(int min, int max) : counter_{ min }, min_{ min }, max_{ max } {} + ThreadCounters(int counter, int min, int max) : counter_{ counter }, min_{ min }, max_{ max } {} + void Reset() { counter_ = min_; } /**< @brief Reset the thread counter to its minimum value. */ }; /** @brief DeepHealth Deep Learning Dataset. @@ -61,158 +325,234 @@ This class extends the DeepHealth Dataset with Deep Learning specific members. */ class DLDataset : public Dataset { +protected: + int batch_size_; /**< @brief Size of each dataset mini batch. */ + std::vector current_batch_; /**< @brief Number of batches already loaded for each split. */ + ColorType ctype_; /**< @brief ecvl::ColorType of the Dataset images. */ + ColorType ctype_gt_; /**< @brief ecvl::ColorType of the Dataset ground truth images. */ + DatasetAugmentations augs_; /**< @brief ecvl::DatasetAugmentations to be applied to the Dataset images (and ground truth if exist) for each split. */ + int num_workers_; /**< @brief Number of parallel workers. */ + ProducersConsumerQueue queue_; /**< @brief Producers-consumer queue of the dataset. */ + std::pair< std::vector, std::vector> tensors_shape_; /**< @brief Shape of sample and label tensors. */ + std::vector> splits_tc_; /**< @brief Each dataset split has its own vector of threads, each of which has its counters: . */ + std::vector producers_; /**< @brief Vector of threads representing the samples producers. */ + bool active_ = false; /**< @brief Whether the threads have already been launched or not. */ + std::mutex active_mutex_; /**< @brief Mutex for active_ variable. */ + static std::default_random_engine re_; /**< @brief Engine used for random number generation. */ + Label* label_ = nullptr; /**< @brief Label pointer which will be specialized based on the dataset task. */ + + /** @brief Set which are the indices of the samples managed by each thread. + + @param[in] split_index index of the split to initialize. + */ + void InitTC(int split_index); + public: - int batch_size_; /**< @brief Size of each dataset mini batch. */ - int n_channels_; /**< @brief Number of channels of the images. */ - int n_channels_gt_; /**< @brief Number of channels of the ground truth images. */ - SplitType current_split_; /**< @brief Current split from which images are loaded. */ - std::vector resize_dims_; /**< @brief Dimensions (HxW) to which Dataset images must be resized. */ - std::array current_batch_ = { 0,0,0 }; /**< @brief Number of batches already loaded for each split. */ - ColorType ctype_; /**< @brief ecvl::ColorType of the Dataset images. */ - ColorType ctype_gt_; /**< @brief ecvl::ColorType of the Dataset ground truth images. */ - DatasetAugmentations augs_; /**< @brief ecvl::DatasetAugmentations to be applied to the Dataset images (and ground truth if exist) for each split. */ - std::mutex mutex_current_batch_; /**< @brief std::mutex to add exclusive access to attribute current_batch_. */ + int n_channels_; /**< @brief Number of channels of the images. */ + int n_channels_gt_ = -1; /**< @brief Number of channels of the ground truth images. */ + std::vector resize_dims_; /**< @brief Dimensions (HxW) to which Dataset images must be resized. */ /** @param[in] filename Path to the Dataset file. @param[in] batch_size Size of each dataset mini batch. - @param[in] augs Array with three DatasetAugmentations (training, validation and test) to be applied to the Dataset images (and ground truth if exists) for each split. - If no augmentation is required or the split doesn't exist, nullptr has to be passed. - @param[in] ctype ecvl::ColorType of the Dataset images. - @param[in] ctype_gt ecvl::ColorType of the Dataset ground truth images. + @param[in] augs Array with DatasetAugmentations to be applied to the Dataset images (and ground truth if exists) for each split. If no augmentation is required nullptr has to be passed. + @param[in] ctype ecvl::ColorType of the Dataset images. Default is RGB. + @param[in] ctype_gt ecvl::ColorType of the Dataset ground truth images. Default is GRAY. + @param[in] num_workers Number of parallel threads spawned. + @param[in] queue_ratio_size The producers-consumer queue will have a maximum size equal to \f$batch\_size \times queue\_ratio\_size \times num\_workers\f$. + @param[in] drop_last For each split, whether to drop the last samples that don't fit the batch size or not. The vector dimensions must match the number of splits. @param[in] verify If true, a list of all the images in the Dataset file which don't exist is printed with an ECVL_WARNING_MSG. */ DLDataset(const filesystem::path& filename, const int batch_size, - DatasetAugmentations augs = DatasetAugmentations(), - ColorType ctype = ColorType::BGR, + const DatasetAugmentations& augs, + ColorType ctype = ColorType::RGB, ColorType ctype_gt = ColorType::GRAY, + int num_workers = 1, + int queue_ratio_size = 1, + vector drop_last = {}, bool verify = false) : Dataset{ filename, verify }, batch_size_{ batch_size }, - augs_(std::move(augs)), + augs_(augs), + num_workers_{ num_workers }, ctype_{ ctype }, - ctype_gt_{ ctype_gt } + ctype_gt_{ ctype_gt }, + queue_{ static_cast(batch_size_ * queue_ratio_size * num_workers_) } { - current_split_ = SplitType::training; - // if training is empty check test and validation and if one of them isn't empty, set it as current split - if (GetSplit(SplitType::training).empty()) { - if (!GetSplit(SplitType::test).empty()) { - current_split_ = SplitType::test; + // resize current_batch_ to the number of splits and initialize it with 0 + current_batch_.resize(split_.size(), 0); + + // Initialize n_channels_ + Image tmp = samples_[0].LoadImage(ctype); + n_channels_ = tmp.Channels(); + + if (!split_.empty()) { + current_split_ = 0; + // Initialize resize_dims_ after that augmentations on the first image are performed + if (augs_.IsEmpty()) { + cout << ECVL_WARNING_MSG << "Augmentations are empty!" << endl; } - else if (!GetSplit(SplitType::validation).empty()) { - current_split_ = SplitType::validation; + else { + while (!augs_.Apply(current_split_, tmp)) { + ++current_split_; + } } + auto y = tmp.channels_.find('y'); + auto x = tmp.channels_.find('x'); + assert(y != std::string::npos && x != std::string::npos); + resize_dims_.insert(resize_dims_.begin(), { tmp.dims_[y],tmp.dims_[x] }); + + // Initialize n_channels_gt_ if exists + if (samples_[0].label_path_ != nullopt) { + n_channels_gt_ = samples_[0].LoadImage(ctype_gt_, true).Channels(); + } + } + else { + cout << ECVL_WARNING_MSG << "Missing splits in the dataset file." << endl; } - Image tmp = this->samples_[0].LoadImage(ctype); - // Initialize resize_dims_ after that augmentations on images are performed - if (!augs_.Apply(SplitType::training, tmp)) { - if (!augs_.Apply(SplitType::validation, tmp)) { - augs_.Apply(SplitType::test, tmp); + // Set drop_last parameter for each split + if (!drop_last.empty() && vsize(drop_last) == vsize(split_)) { + for (int i = 0; i < vsize(drop_last); ++i) { + split_[i].drop_last_ = drop_last[i]; } } - auto y = tmp.channels_.find('y'); - auto x = tmp.channels_.find('x'); - assert(y != std::string::npos && x != std::string::npos); - resize_dims_.insert(resize_dims_.begin(), { tmp.dims_[y],tmp.dims_[x] }); - // Initialize n_channels_ - n_channels_ = tmp.Channels(); + // Initialize num_batches, last_batch and the ThreadCounters for each split + auto s_index = 0; + splits_tc_ = std::vector>(vsize(split_)); + for (auto& s : split_) { + s.SetNumBatches(batch_size_); + s.SetLastBatch(batch_size_); - // Initialize n_channels_gt_ if exists - if (!GetSplit().empty()) { - if (samples_[GetSplit()[0]].label_path_ != nullopt) { - n_channels_gt_ = samples_[GetSplit()[0]].LoadImage(ctype_gt_, true).Channels(); - } + InitTC(s_index); + ++s_index; + } + + switch (task_) { + case Task::classification: + label_ = new LabelClass(); + tensors_shape_ = make_pair, vector>({ batch_size_, n_channels_, resize_dims_[0], resize_dims_[1] }, { batch_size_, vsize(classes_) }); + break; + case Task::segmentation: + label_ = new LabelImage(); + tensors_shape_ = make_pair, vector>({ batch_size_, n_channels_, resize_dims_[0], resize_dims_[1] }, + { batch_size_, n_channels_gt_, resize_dims_[0], resize_dims_[1] }); + break; } } - /** @brief Returns the image indexes of the current Split. - @return vector of image indexes of the Split in use. - */ - std::vector& GetSplit(); + /* Destructor */ + ~DLDataset() + { + delete label_; + } - /** @brief Returns the image indexes of the requested Split. - @param[in] split ecvl::SplitType representing the Split to get ("training", "validation", or "test"). - @return vector of image indexes of the requested Split. - */ - std::vector& GetSplit(const SplitType& split); + /** @brief Reset the batch counter and optionally shuffle samples indices of the specified split. - /** @brief Reset the batch counter of the current Split. */ - void ResetCurrentBatch(); + If no split is provided or an illegal value is provided, the current split is reset. + @param[in] split_index index, name or SplitType of the split to reset. + @param[in] shuffle boolean which indicates whether to shuffle the split samples indices or not. + */ + void ResetBatch(const ecvl::any& split = -1, bool shuffle = false); - /** @brief Reset the batch counter of each Split. */ - void ResetAllBatches(); + /** @brief Reset the batch counter of each split and optionally shuffle samples indices (within each split). - /** @brief Set the current Split. - @param[in] split ecvl::SplitType representing the Split to set ("training", "validation", or "test"). + @param[in] shuffle boolean which indicates whether to shuffle the samples indices or not. */ - void SetSplit(const SplitType& split); + void ResetAllBatches(bool shuffle = false); /** @brief Load a batch into _images_ and _labels_ `tensor`. + @param[out] images `tensor` which stores the batch of images. @param[out] labels `tensor` which stores the batch of labels. */ void LoadBatch(Tensor*& images, Tensor*& labels); /** @brief Load a batch into _images_ `tensor`. Useful for tests set when you don't have labels. + @param[out] images `tensor` which stores the batch of images. */ void LoadBatch(Tensor*& images); -}; -/** @brief Convert an EDDL Tensor into an ECVL Image. + /** @brief Set a fixed seed for the random generated values. Useful to reproduce experiments with same shuffling during training. -Tensor dimensions must be \f$C\f$ x \f$H\f$ x \f$W\f$ or \f$N\f$ x \f$C\f$ x \f$H\f$ x \f$W\f$, where: \n -\f$N\f$ = batch size \n -\f$C\f$ = channels \n -\f$H\f$ = height \n -\f$W\f$ = width + @param[in] seed Value of the seed for the random engine. + */ + static void SetSplitSeed(unsigned seed) { re_.seed(seed); } -@param[in] t Input EDDL Tensor. -@param[out] img Output ECVL Image. It is a "xyo" with DataType::float32 and ColorType::none Image. + /** @brief Set a new batch size inside the dataset. -*/ -void TensorToImage(Tensor*& t, Image& img); + Notice that this will not affect the EDDL network batch size, that it has to be changed too. + @param[in] bs Value to set for the batch size. + */ + void SetBatchSize(int bs); -/** @brief Convert an EDDL Tensor into an ECVL View. + /** @brief Load a sample and its label, and push them to the producers-consumer queue. -Tensor dimensions must be \f$C\f$ x \f$H\f$ x \f$W\f$ or \f$N\f$ x \f$C\f$ x \f$H\f$ x \f$W\f$, where: \n -\f$N\f$ = batch size \n -\f$C\f$ = channels \n -\f$H\f$ = height \n -\f$W\f$ = width + @param[in] elem Sample to load and push to the queue. -@param[in] t Input EDDL Tensor. -@param[out] v Output ECVL View. It is a "xyo" with ColorType::none View. + @anchor ProduceImageLabel + */ + virtual void ProduceImageLabel(DatasetAugmentations& augs, Sample& elem); -*/ -void TensorToView(Tensor*& t, View& v); + /** @brief Function called when the thread are spawned. -/** @brief Convert an ECVL Image into an EDDL Tensor. + @ref ProduceImageLabel is called for each sample under the competence of the thread. -Image must have 3 dimensions "xy[czo]" (in any order). \n -Output Tensor will be created with shape \f$C\f$ x \f$H\f$ x \f$W\f$. \n + @param[in] thread_index index of the thread. + */ + void ThreadFunc(int thread_index); -@param[in] img Input ECVL Image. -@param[out] t Output EDDL Tensor. It is created inside the function. + /** @brief Pop batch_size samples from the queue and copy them into EDDL tensors. -*/ -void ImageToTensor(const Image& img, Tensor*& t); + @return tuples of Samples and EDDL Tensors, the first with the image and the second with the label. + */ + std::tuple, unique_ptr, unique_ptr> GetBatch(); -/** @brief Insert an ECVL Image into an EDDL Tensor. + /** @brief Spawn num_workers thread. -This function is useful to insert into an EDDL Tensor more than one image, specifying how many images are already stored in the Tensor. -Image must have 3 dimensions "xy[czo]" (in any order). \n + @param[in] split_index Index of the split to use in the GetBatch function. If not specified, current split is used. + */ + void Start(int split_index = -1); -@param[in] img Input ECVL Image. -@param[out] t Output EDDL Tensor. It must be created with the right dimensions before calling this function. -@param[in] offset How many images are already stored in the Tensor. + /** @brief Join all the threads. */ + void Stop(); + + /** @brief Get the current size of the producers-consumer queue of the dataset. + @return Size of the producers-consumer queue of the dataset. + */ + auto GetQueueSize() const { return queue_.Length(); }; + + /** @brief Set the dataset augmentations. + + @param[in] da @ref DatasetAugmentations to set. + */ + void SetAugmentations(const DatasetAugmentations& da); + + /** @brief Get the number of batches of the specified split. + + If no split is provided or an illegal value is provided, the number of batches of the current split is returned. + @param[in] split index, name or ecvl::SplitType representing the split from which to get the number of batches. + @return number of batches of the specified split. + */ + const int GetNumBatches(const ecvl::any& split = -1); +}; + +/** @brief Make a grid of images from a EDDL Tensor. + +Return a grid of Image from a EDDL Tensor. + +@param[in] t Input EDDL Tensor of shape (B x C x H x W). +@param[in] cols Number of images displayed in each row of the grid. +@param[in] normalize If true, shift the image to the range [0,1]. + +@return Image that contains the grid of images */ -void ImageToTensor(const Image& img, Tensor*& t, const int& offset); +Image MakeGrid(Tensor*& t, int cols = 8, bool normalize = false); /** @example example_ecvl_eddl.cpp Example of using ECVL with EDDL. diff --git a/modules/eddl/src/support_eddl.cpp b/modules/eddl/src/support_eddl.cpp index 04b00398..fd68e090 100644 --- a/modules/eddl/src/support_eddl.cpp +++ b/modules/eddl/src/support_eddl.cpp @@ -18,6 +18,7 @@ #include "ecvl/core/imgproc.h" #include "ecvl/core/standard_errors.h" +#include #include using namespace eddl; @@ -25,7 +26,12 @@ using namespace ecvl::filesystem; namespace ecvl { -void TensorToImage(Tensor*& t, Image& img) +#define ECVL_ERROR_START_ALREADY_ACTIVE throw std::runtime_error(ECVL_ERROR_MSG "Trying to start the producer threads when they are already running!"); +#define ECVL_ERROR_STOP_ALREADY_END throw std::runtime_error(ECVL_ERROR_MSG "Trying to stop the producer threads when they are already ended!"); +#define ECVL_ERROR_WORKERS_LESS_THAN_ONE throw std::runtime_error(ECVL_ERROR_MSG "Dataset workers must be at least one"); +default_random_engine DLDataset::re_(random_device{}()); + +void TensorToImage(const Tensor* t, Image& img) { switch (t->ndim) { case 3: @@ -42,7 +48,7 @@ void TensorToImage(Tensor*& t, Image& img) memcpy(img.data_, t->ptr, img.datasize_); } -void TensorToView(Tensor*& t, View& v) +void TensorToView(const Tensor* t, View& v) { switch (t->ndim) { case 3: @@ -135,7 +141,7 @@ void ImageToTensor(const Image& img, Tensor*& t, const int& offset) tot_dims = accumulate(img.dims_.begin(), img.dims_.end(), 1, std::multiplies()); // Check if the current image exceeds the total size of the tensor - if (t->size < tot_dims * (offset + 1)) { + if (t->size < static_cast(tot_dims * (offset + 1))) { cerr << ECVL_ERROR_MSG "Size of the images exceeds those of the tensor" << endl; ECVL_ERROR_INCOMPATIBLE_DIMENSIONS } @@ -143,50 +149,32 @@ void ImageToTensor(const Image& img, Tensor*& t, const int& offset) memcpy(t->ptr + tot_dims * offset, tmp.data_, tot_dims * sizeof(float)); } -std::vector& DLDataset::GetSplit(const SplitType& split) +void DLDataset::ResetBatch(const any& split, bool shuffle) { - if (split == SplitType::training) { - return this->split_.training_; - } - else if (split == SplitType::validation) { - return this->split_.validation_; - } - else { - return this->split_.test_; - } -} + int index = GetSplitIndex(split); + this->current_batch_.at(index) = 0; -std::vector& DLDataset::GetSplit() -{ - return GetSplit(current_split_); -} - -void DLDataset::SetSplit(const SplitType& split) -{ - if (GetSplit(split).size() > 0) { - this->current_split_ = split; + if (shuffle) { + std::shuffle(begin(GetSplit(index)), end(GetSplit(index)), re_); } - else { - ECVL_ERROR_SPLIT_DOES_NOT_EXIST - } -} -void DLDataset::ResetCurrentBatch() -{ - { // CRITICAL REGION STARTS - std::unique_lock lck(mutex_current_batch_); - - this->current_batch_[+current_split_] = 0; - } // CRITICAL REGION ENDS + for (auto& tc : splits_tc_[index]) { + tc.Reset(); + } } -void DLDataset::ResetAllBatches() +void DLDataset::ResetAllBatches(bool shuffle) { - { // CRITICAL REGION STARTS - std::unique_lock lck(mutex_current_batch_); + fill(current_batch_.begin(), current_batch_.end(), 0); - this->current_batch_.fill(0); - } // CRITICAL REGION ENDS + if (shuffle) { + for (int split_index = 0; split_index < vsize(split_); ++split_index) { + std::shuffle(begin(GetSplit(split_index)), end(GetSplit(split_index)), re_); + for (auto& tc : splits_tc_[split_index]) { + tc.Reset(); + } + } + } } void DLDataset::LoadBatch(Tensor*& images, Tensor*& labels) @@ -221,12 +209,9 @@ void DLDataset::LoadBatch(Tensor*& images, Tensor*& labels) } // Move to next samples - { // CRITICAL REGION STARTS - std::unique_lock lck(mutex_current_batch_); - start = current_batch_[+current_split_] * bs; - ++current_batch_[+current_split_]; - } // CRITICAL REGION ENDS + start = current_batch_[current_split_] * bs; + ++current_batch_[current_split_]; if (vsize(GetSplit()) < start + bs) { cerr << ECVL_ERROR_MSG "Batch size is not even with the number of samples. Hint: loop through `num_batches = num_samples / batch_size;`" << endl; @@ -290,12 +275,9 @@ void DLDataset::LoadBatch(Tensor*& images) } // Move to next samples - { // CRITICAL REGION STARTS - std::unique_lock lck(mutex_current_batch_); - start = current_batch_[+current_split_] * bs; - ++current_batch_[+current_split_]; - } // CRITICAL REGION ENDS + start = current_batch_[current_split_] * bs; + ++current_batch_[current_split_]; if (vsize(GetSplit()) < start + bs) { cerr << ECVL_ERROR_MSG "Batch size is not even with the number of samples. Hint: loop through `num_batches = num_samples / batch_size;`" << endl; @@ -318,4 +300,245 @@ void DLDataset::LoadBatch(Tensor*& images) ++offset; } } + +Image MakeGrid(Tensor*& t, int cols, bool normalize) +{ + const auto batch_size = t->shape[0]; + cols = std::min(batch_size, cols); + const auto rows = static_cast(std::ceil(static_cast(batch_size) / cols)); + + Image image_t; + vector vimages; + for (int r = 0, b = 0; r < rows; ++r) { + vector himages; + for (int c = 0; c < cols; ++c) { + Tensor* tensor_t; + if (b < batch_size) { + tensor_t = t->select({ to_string(b) }); + TensorToImage(tensor_t, image_t); + if (normalize) { + ScaleTo(image_t, image_t, 0, 1); + } + image_t.Mul(255.); + image_t.channels_ = "xyc"; + image_t.ConvertTo(DataType::uint8); + delete tensor_t; + } + else { + image_t = Image({ t->shape[3],t->shape[2],t->shape[1] }, DataType::uint8, "xyc", ColorType::none); + image_t.SetTo(0); + } + himages.push_back(image_t); + ++b; + } + if (himages.size() > 1) { + HConcat(himages, image_t); + } + vimages.push_back(image_t); + } + if (vimages.size() > 1) { + VConcat(vimages, image_t); + } + return image_t; +} + +void DLDataset::ProduceImageLabel(DatasetAugmentations& augs, Sample& elem) +{ + Image img = elem.LoadImage(ctype_, false); + switch (task_) { + case Task::classification: + { + LabelClass* label = nullptr; + // Read the label + if (!split_[current_split_].no_label_) { + label = new LabelClass(); + label->label = elem.label_.value(); + } + // Apply chain of augmentations only to sample image + augs.Apply(current_split_, img); + queue_.Push(elem, img, label); + } + break; + case Task::segmentation: + { + LabelImage* label = nullptr; + // Read the ground truth + if (!split_[current_split_].no_label_) { + label = new LabelImage(); + Image gt = elem.LoadImage(ctype_gt_, true); + // Apply chain of augmentations to sample image and corresponding ground truth + augs.Apply(current_split_, img, gt); + label->gt = gt; + } + else { + augs.Apply(current_split_, img); + } + queue_.Push(elem, img, label); + } + break; + } +} + +void DLDataset::InitTC(int split_index) +{ + auto& split_indexes = split_[split_index].samples_indices_; + auto& drop_last = split_[split_index].drop_last_; + auto samples_per_queue = vsize(split_indexes) / num_workers_; + auto exceeding_samples = vsize(split_indexes) % num_workers_ * !drop_last; + + // Set which are the indices of the samples managed by each thread + // The i-th thread manage samples from start to end + std::vector split_tc; + for (auto i = 0; i < num_workers_; ++i) { + auto start = samples_per_queue * i; + auto end = start + samples_per_queue; + if (i >= num_workers_ - 1) { + // The last thread takes charge of exceeding samples + end += exceeding_samples; + } + split_tc.push_back(ThreadCounters(start, end)); + } + + splits_tc_[split_index] = split_tc; +} + +void DLDataset::ThreadFunc(int thread_index) +{ + auto& tc_of_current_split = splits_tc_[current_split_]; + DatasetAugmentations augs = augs_; + while (tc_of_current_split[thread_index].counter_ < tc_of_current_split[thread_index].max_) { + auto sample_index = split_[current_split_].samples_indices_[tc_of_current_split[thread_index].counter_]; + Sample& elem = samples_[sample_index]; + + ProduceImageLabel(augs, elem); + + ++tc_of_current_split[thread_index].counter_; + + std::unique_lock lock(active_mutex_); + if (!active_) { + return; + } + } +} + +tuple, unique_ptr, unique_ptr> DLDataset::GetBatch() +{ + if (!active_) { + cout << ECVL_WARNING_MSG << "You're trying to get a batch without starting the threads - you'll wait forever!" << endl; + } + + ++current_batch_[current_split_]; + auto& s = split_[current_split_]; + auto tensors_shape = tensors_shape_; + + // Reduce batch size for the last batch in the split + if (current_batch_[current_split_] == s.num_batches_) { + tensors_shape.first[0] = s.last_batch_; + if (!s.no_label_) { + tensors_shape.second[0] = s.last_batch_; + } + } + + // If current split has no labels (e.g., test split could have no labels) set y as empty tensor + tensors_shape.second = (s.no_label_) ? vector{} : tensors_shape.second; + + unique_ptr x = make_unique(tensors_shape.first); + unique_ptr y = make_unique(tensors_shape.second); + + const int batch_len = x->shape[0]; + Image img; + vector samples(batch_len); + for (int i = 0; i < batch_len; ++i) { + queue_.Pop(samples[i], img, label_); // Consumer get samples from the queue + + // Copy sample image into tensor + auto lhs = x.get(); + ImageToTensor(img, lhs, i); + + if (label_ != nullptr) { // Label nullptr means no label at all for this sample (example: possible for test split) + // Copy label into tensor + label_->ToTensorPlane(y.get(), i); + delete label_; + label_ = nullptr; + } + } + + return make_tuple(move(samples), move(x), move(y)); +} + +void DLDataset::Start(int split_index) +{ + if (active_) { + ECVL_ERROR_START_ALREADY_ACTIVE + } + + active_ = true; + + if (split_index != -1 && split_index != current_split_) { + SetSplit(split_index); + } + + producers_.clear(); + queue_.Clear(); + + if (num_workers_ > 0) { + for (int i = 0; i < num_workers_; ++i) { + producers_.push_back(std::thread(&DLDataset::ThreadFunc, this, i)); + } + } + else { + ECVL_ERROR_WORKERS_LESS_THAN_ONE + } +} + +void DLDataset::Stop() +{ + if (!active_) { + ECVL_ERROR_STOP_ALREADY_END + } + + active_ = false; + for (int i = 0; i < num_workers_; ++i) { + producers_[i].join(); + } +} + +const int DLDataset::GetNumBatches(const any& split) +{ + auto it = GetSplitIt(split); + return it->num_batches_; +} + +void DLDataset::SetAugmentations(const DatasetAugmentations& da) +{ + augs_ = da; + + // Initialize resize_dims_ after that augmentations on the first image are performed + Image tmp = samples_[0].LoadImage(ctype_); + augs_.Apply(current_split_, tmp); + auto y = tmp.channels_.find('y'); + auto x = tmp.channels_.find('x'); + assert(y != std::string::npos && x != std::string::npos); + resize_dims_[0] = tmp.dims_[y]; + resize_dims_[1] = tmp.dims_[x]; +} + +void DLDataset::SetBatchSize(int bs) +{ + // check if the provided batch size is negative or greater than the current split size + if (bs > 0 && bs < vsize(split_[current_split_].samples_indices_)) { + int new_queue_size = static_cast(queue_.Length() / batch_size_ * bs); + batch_size_ = bs; + tensors_shape_.first[0] = batch_size_; + tensors_shape_.second[0] = batch_size_; + queue_.SetSize(new_queue_size); + for (auto& s : split_) { + s.SetNumBatches(batch_size_); + s.SetLastBatch(batch_size_); + } + } + else { + ECVL_ERROR_WRONG_PARAMS("bs in SetBatchSize") + } +} } // namespace ecvl \ No newline at end of file diff --git a/modules/eddl/test/test_eddl.cpp b/modules/eddl/test/test_eddl.cpp index 20b9f341..480d2e88 100644 --- a/modules/eddl/test/test_eddl.cpp +++ b/modules/eddl/test/test_eddl.cpp @@ -1,4 +1,4 @@ - /* +/* * ECVL - European Computer Vision Library * Version: 0.3.4 * copyright (c) 2021, Università degli Studi di Modena e Reggio Emilia (UNIMORE), AImageLab @@ -19,104 +19,164 @@ #include "ecvl/augmentations.h" using namespace ecvl; +using std::stringstream; +using std::unique_ptr; +using std::runtime_error; TEST(Augmentations, ConstructFromStreamAllParamsOk) { Image img({ 5, 5, 1 }, DataType::uint8, "xyc", ColorType::GRAY); - std::unique_ptr p; - std::stringstream ss("angle=[-5,5] center=(0,0) scale=0.5 interp=\"linear\""); + unique_ptr p; + stringstream ss("angle=[-5,5] center=(0,0) scale=0.5 interp=\"linear\""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("dims=(100,100) interp=\"linear\""); + ss = stringstream("dims=(100,100) interp=\"linear\""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("scale=(1.,2.) interp=\"linear\""); + ss = stringstream("scale=(1.,2.) interp=\"linear\""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("p=0.3"); + ss = stringstream("p=0.3"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("p=0.3"); + ss = stringstream("p=0.3"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("sigma=[1.,2.]"); + ss = stringstream("sigma=[1.,2.]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("std_dev=[1.,2.]"); + ss = stringstream("std_dev=[1.,2.]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("lambda=[1.,2.]"); + ss = stringstream("lambda=[1.,2.]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("gamma=[1.,2.]"); + ss = stringstream("gamma=[1.,2.]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("p=[0,0.55] drop_size=[0.02,0.1] per_channel=0"); + ss = stringstream("p=[0,0.55] drop_size=[0.02,0.1] per_channel=0"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("p=0.4"); + ss = stringstream("p=0.4"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("beta=[30,60]"); + ss = stringstream("beta=[30,60]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("num_steps=[5,10] distort_limit=[-0.2,0.2] interp=\"linear\" border_type=\"reflect_101\" border_value=0"); + ss = stringstream("num_steps=[5,10] distort_limit=[-0.2,0.2] interp=\"linear\" border_type=\"reflect_101\" border_value=0"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("alpha=[34,60] sigma=[4,6] interp=\"linear\" border_type=\"reflect_101\" border_value=0"); + ss = stringstream("alpha=[34,60] sigma=[4,6] interp=\"linear\" border_type=\"reflect_101\" border_value=0"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("distort_limit=[5,10] shift_limit=[4,6] interp=\"linear\" border_type=\"reflect_101\" border_value=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("mean=100 std=1"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream(""); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("size=(100,100)"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("divisor=255 divisor_gt=255"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream(""); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("new_min=0 new_max=1"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); } TEST(Augmentations, ConstructFromStreamWithoutOptionalParms) { Image img({ 5, 5, 1 }, DataType::uint8, "xyc", ColorType::GRAY); - std::unique_ptr p; - std::stringstream ss("angle=[-5,5]"); + unique_ptr p; + stringstream ss("angle=[-5,5]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("dims=(100,100)"); + ss = stringstream("dims=(100,100)"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("scale=(1.,2.)"); + ss = stringstream("scale=(1.,2.)"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream(""); + ss = stringstream(""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream(""); + ss = stringstream(""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream(""); + ss = stringstream(""); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("beta=[30,60]"); + ss = stringstream("beta=[30,60]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("num_steps=[5,10] distort_limit=[-0.2,0.2]"); + ss = stringstream("num_steps=[5,10] distort_limit=[-0.2,0.2]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); - ss = std::stringstream("alpha=[34,60] sigma=[4,6]"); + ss = stringstream("alpha=[34,60] sigma=[4,6]"); EXPECT_NO_THROW(p = make_unique(ss)); EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("distort_limit=[5,10] shift_limit=[4,6]"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("p=[0,0.55] per_channel=0"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("mean=100 std=1"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("size=(100,100)"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("divisor=255"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream(""); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); + ss = stringstream("new_min=0 new_max=1"); + EXPECT_NO_THROW(p = make_unique(ss)); + EXPECT_NO_THROW(p->Apply(img)); } TEST(Augmentations, ConstructFromStreamWithWrongParms) { Image img({ 5, 5, 1 }, DataType::uint8, "xyc", ColorType::GRAY); - std::unique_ptr p; - std::stringstream ss("angle=(-5,5)"); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream("dims=100"); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream(""); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream("p=\"test\""); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream(""); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream("num_steps=[5,10] distort_limit=(-0.2,0.2)"); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); - ss = std::stringstream("alpha=34"); - EXPECT_THROW(p = make_unique(ss), std::runtime_error); + unique_ptr p; + stringstream ss("angle=(-5,5)"); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream("dims=100"); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream(""); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream("p=\"test\""); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream(""); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream("num_steps=[5,10] distort_limit=(-0.2,0.2)"); + EXPECT_THROW(p = make_unique(ss), runtime_error); + ss = stringstream("alpha=34"); + EXPECT_THROW(p = make_unique(ss), runtime_error); } \ No newline at end of file