From be91ec82c295c706bb25688ebcd206840033483c Mon Sep 17 00:00:00 2001 From: TankredO Date: Fri, 23 Apr 2021 14:37:14 +0200 Subject: [PATCH] pre-release --- .github/workflows/pt15_cu101_manual.yml | 58 + .../workflows/pt15_cu101_manual_nightly.yml | 59 + .github/workflows/pt16_cu101_manual.yml | 58 + .../workflows/pt16_cu101_manual_nightly.yml | 59 + .github/workflows/pt16_cu102_manual.yml | 58 + .../workflows/pt16_cu102_manual_nightly.yml | 59 + .github/workflows/pt16_cu92_manual.yml | 58 + .../workflows/pt16_cu92_manual_nightly.yml | 59 + .github/workflows/pt17_cu101_manual.yml | 58 + .../workflows/pt17_cu101_manual_nightly.yml | 59 + .github/workflows/pt17_cu102_manual.yml | 58 + .../workflows/pt17_cu102_manual_nightly.yml | 59 + .github/workflows/pt17_cu110_manual.yml | 58 + .../workflows/pt17_cu110_manual_nightly.yml | 59 + .github/workflows/pt17_cu92_manual.yml | 58 + .../workflows/pt17_cu92_manual_nightly.yml | 59 + .github/workflows/pt18_cu101_manual.yml | 55 + .../workflows/pt18_cu101_manual_nightly.yml | 56 + .github/workflows/pt18_cu102_manual.yml | 55 + .../workflows/pt18_cu102_manual_nightly.yml | 56 + .github/workflows/pt18_cu111_manual.yml | 55 + .../workflows/pt18_cu111_manual_nightly.yml | 56 + .github/workflows/test-build.yml | 60 + .github/workflows/trigger_builds.yml | 21 + .github/workflows/trigger_nightly_builds.yml | 21 + .gitignore | 141 ++ .pylintrc | 611 ++++++ LICENSE | 201 ++ README.md | 130 ++ .../ginjinn-gpu-pt15_cu101_linux/build.sh | 1 + .../ginjinn-gpu-pt15_cu101_linux/meta.yaml | 40 + .../ginjinn-gpu-pt15_cu101_linux/post-link.sh | 1 + .../ginjinn-gpu-pt16_cu101_linux/build.sh | 1 + .../ginjinn-gpu-pt16_cu101_linux/meta.yaml | 42 + .../ginjinn-gpu-pt16_cu101_linux/post-link.sh | 1 + .../ginjinn-gpu-pt16_cu102_linux/build.sh | 1 + .../ginjinn-gpu-pt16_cu102_linux/meta.yaml | 43 + .../ginjinn-gpu-pt16_cu102_linux/post-link.sh | 1 + .../ginjinn-gpu-pt16_cu92_linux/build.sh | 1 + .../ginjinn-gpu-pt16_cu92_linux/meta.yaml | 41 + .../ginjinn-gpu-pt16_cu92_linux/post-link.sh | 1 + .../ginjinn-gpu-pt17_cu101_linux/build.sh | 1 + .../ginjinn-gpu-pt17_cu101_linux/meta.yaml | 42 + .../ginjinn-gpu-pt17_cu101_linux/post-link.sh | 1 + .../ginjinn-gpu-pt17_cu102_linux/build.sh | 1 + .../ginjinn-gpu-pt17_cu102_linux/meta.yaml | 42 + .../ginjinn-gpu-pt17_cu102_linux/post-link.sh | 1 + .../ginjinn-gpu-pt17_cu110_linux/build.sh | 1 + .../ginjinn-gpu-pt17_cu110_linux/meta.yaml | 42 + .../ginjinn-gpu-pt17_cu110_linux/post-link.sh | 1 + .../ginjinn-gpu-pt17_cu92_linux/build.sh | 1 + .../ginjinn-gpu-pt17_cu92_linux/meta.yaml | 41 + .../ginjinn-gpu-pt17_cu92_linux/post-link.sh | 1 + .../ginjinn-gpu-pt18_cu101_linux/build.sh | 1 + .../ginjinn-gpu-pt18_cu101_linux/meta.yaml | 42 + .../ginjinn-gpu-pt18_cu101_linux/post-link.sh | 1 + .../ginjinn-gpu-pt18_cu102_linux/build.sh | 1 + .../ginjinn-gpu-pt18_cu102_linux/meta.yaml | 42 + .../ginjinn-gpu-pt18_cu102_linux/post-link.sh | 1 + .../ginjinn-gpu-pt18_cu111_linux/build.sh | 1 + .../ginjinn-gpu-pt18_cu111_linux/meta.yaml | 42 + .../ginjinn-gpu-pt18_cu111_linux/post-link.sh | 1 + ginjinn/__init__.py | 25 + ginjinn/__main__.py | 7 + ginjinn/commandline/__init__.py | 5 + ginjinn/commandline/argument_parser.py | 1654 +++++++++++++++++ ginjinn/commandline/commandline_app.py | 100 + ginjinn/commandline/evaluate.py | 104 ++ ginjinn/commandline/info.py | 41 + ginjinn/commandline/main.py | 13 + ginjinn/commandline/new.py | 239 +++ ginjinn/commandline/predict.py | 115 ++ ginjinn/commandline/simulate.py | 124 ++ ginjinn/commandline/splitter.py | 128 ++ .../commandline/tests/test_argument_parser.py | 30 + ginjinn/commandline/tests/test_cmd_main.py | 305 +++ ginjinn/commandline/train.py | 138 ++ ginjinn/commandline/utils.py | 750 ++++++++ ginjinn/data/example_data.txt | 1 + .../data/ginjinn_config/example_config_0.yaml | 71 + .../data/ginjinn_config/example_config_1.yaml | 70 + .../data/ginjinn_config/template_config.yaml | 41 + .../adv_faster_rcnn_R_101_C4_3x.yaml | 86 + .../adv_faster_rcnn_R_101_DC5_3x.yaml | 86 + .../adv_faster_rcnn_R_101_FPN_3x.yaml | 86 + .../templates/adv_faster_rcnn_R_50_C4_1x.yaml | 84 + .../templates/adv_faster_rcnn_R_50_C4_3x.yaml | 86 + .../adv_faster_rcnn_R_50_DC5_1x.yaml | 86 + .../adv_faster_rcnn_R_50_DC5_3x.yaml | 86 + .../adv_faster_rcnn_R_50_FPN_1x.yaml | 86 + .../adv_faster_rcnn_R_50_FPN_3x.yaml | 86 + .../adv_faster_rcnn_X_101_32x8d_FPN_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_101_C4_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_101_DC5_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_101_FPN_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_50_C4_1x.yaml | 85 + .../templates/adv_mask_rcnn_R_50_C4_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_50_DC5_1x.yaml | 86 + .../templates/adv_mask_rcnn_R_50_DC5_3x.yaml | 86 + .../templates/adv_mask_rcnn_R_50_FPN_1x.yaml | 86 + .../templates/adv_mask_rcnn_R_50_FPN_3x.yaml | 86 + .../adv_mask_rcnn_X_101_32x8d_FPN_3x.yaml | 86 + .../templates/faster_rcnn_R_101_C4_3x.yaml | 56 + .../templates/faster_rcnn_R_101_DC5_3x.yaml | 56 + .../templates/faster_rcnn_R_101_FPN_3x.yaml | 56 + .../templates/faster_rcnn_R_50_C4_1x.yaml | 56 + .../templates/faster_rcnn_R_50_C4_3x.yaml | 56 + .../templates/faster_rcnn_R_50_DC5_1x.yaml | 56 + .../templates/faster_rcnn_R_50_DC5_3x.yaml | 56 + .../templates/faster_rcnn_R_50_FPN_1x.yaml | 56 + .../templates/faster_rcnn_R_50_FPN_3x.yaml | 56 + .../faster_rcnn_X_101_32x8d_FPN_3x.yaml | 56 + .../templates/mask_rcnn_R_101_C4_3x.yaml | 56 + .../templates/mask_rcnn_R_101_DC5_3x.yaml | 56 + .../templates/mask_rcnn_R_101_FPN_3x.yaml | 56 + .../templates/mask_rcnn_R_50_C4_1x.yaml | 54 + .../templates/mask_rcnn_R_50_C4_3x.yaml | 86 + .../templates/mask_rcnn_R_50_DC5_1x.yaml | 56 + .../templates/mask_rcnn_R_50_DC5_3x.yaml | 56 + .../templates/mask_rcnn_R_50_FPN_1x.yaml | 56 + .../templates/mask_rcnn_R_50_FPN_3x.yaml | 56 + .../mask_rcnn_X_101_32x8d_FPN_3x.yaml | 56 + ginjinn/data_reader/__init__.py | 0 ginjinn/data_reader/data_error.py | 10 + ginjinn/data_reader/data_reader.py | 249 +++ ginjinn/data_reader/data_splitter.py | 624 +++++++ ginjinn/data_reader/load_datasets.py | 205 ++ ginjinn/data_reader/merge_datasets.py | 260 +++ .../data_reader/tests/test_data_splitter.py | 49 + ginjinn/evaluation/__init__.py | 4 + ginjinn/evaluation/evaluate.py | 87 + ginjinn/ginjinn_config/__init__.py | 12 + ginjinn/ginjinn_config/augmentation_config.py | 922 +++++++++ ginjinn/ginjinn_config/config_error.py | 26 + ginjinn/ginjinn_config/detectron_config.py | 76 + ginjinn/ginjinn_config/ginjinn_config.py | 206 ++ ginjinn/ginjinn_config/input_config.py | 356 ++++ ginjinn/ginjinn_config/model_config.py | 788 ++++++++ ginjinn/ginjinn_config/options_config.py | 122 ++ .../tests/test_augmentation_configuration.py | 440 +++++ .../tests/test_ginjinn_config.py | 254 +++ .../tests/test_input_configuration.py | 343 ++++ .../tests/test_model_configuration.py | 65 + .../tests/test_options_config.py | 75 + .../tests/test_read_example_config.py | 14 + .../ginjinn_config/tests/test_train_config.py | 77 + ginjinn/ginjinn_config/training_config.py | 210 +++ ginjinn/predictor/__init__.py | 4 + ginjinn/predictor/predictors.py | 480 +++++ ginjinn/predictor/tests/test_predictors.py | 8 + ginjinn/segmentation_refinement/LICENSE | 21 + ginjinn/segmentation_refinement/NOTE.txt | 3 + ginjinn/segmentation_refinement/__init__.py | 1 + ginjinn/segmentation_refinement/download.py | 30 + .../segmentation_refinement/eval_helper.py | 192 ++ ginjinn/segmentation_refinement/main.py | 76 + .../models/__init__.py | 0 .../models/psp/__init__.py | 0 .../models/psp/extractors.py | 108 ++ .../models/psp/pspnet.py | 171 ++ ginjinn/simulation/__init__.py | 4 + ginjinn/simulation/coco_utils.py | 222 +++ ginjinn/simulation/pvoc_utils.py | 126 ++ ginjinn/simulation/shapes.py | 211 +++ ginjinn/simulation/simulation.py | 279 +++ ginjinn/simulation/tests/test_simulation.py | 60 + ginjinn/simulation/utils.py | 27 + ginjinn/test_pytest.py | 11 + ginjinn/tests/test_main.py | 12 + ginjinn/trainer/__init__.py | 4 + ginjinn/trainer/tests/test_trainer.py | 119 ++ ginjinn/trainer/trainer.py | 510 +++++ ginjinn/utils/__init__.py | 9 + ginjinn/utils/data_prep.py | 384 ++++ ginjinn/utils/dataset_cropping.py | 1045 +++++++++++ ginjinn/utils/sliding_window_merging.py | 1001 ++++++++++ ginjinn/utils/utils.py | 1126 +++++++++++ setup.py | 51 + 178 files changed, 21530 insertions(+) create mode 100644 .github/workflows/pt15_cu101_manual.yml create mode 100644 .github/workflows/pt15_cu101_manual_nightly.yml create mode 100644 .github/workflows/pt16_cu101_manual.yml create mode 100644 .github/workflows/pt16_cu101_manual_nightly.yml create mode 100644 .github/workflows/pt16_cu102_manual.yml create mode 100644 .github/workflows/pt16_cu102_manual_nightly.yml create mode 100644 .github/workflows/pt16_cu92_manual.yml create mode 100644 .github/workflows/pt16_cu92_manual_nightly.yml create mode 100644 .github/workflows/pt17_cu101_manual.yml create mode 100644 .github/workflows/pt17_cu101_manual_nightly.yml create mode 100644 .github/workflows/pt17_cu102_manual.yml create mode 100644 .github/workflows/pt17_cu102_manual_nightly.yml create mode 100644 .github/workflows/pt17_cu110_manual.yml create mode 100644 .github/workflows/pt17_cu110_manual_nightly.yml create mode 100644 .github/workflows/pt17_cu92_manual.yml create mode 100644 .github/workflows/pt17_cu92_manual_nightly.yml create mode 100644 .github/workflows/pt18_cu101_manual.yml create mode 100644 .github/workflows/pt18_cu101_manual_nightly.yml create mode 100644 .github/workflows/pt18_cu102_manual.yml create mode 100644 .github/workflows/pt18_cu102_manual_nightly.yml create mode 100644 .github/workflows/pt18_cu111_manual.yml create mode 100644 .github/workflows/pt18_cu111_manual_nightly.yml create mode 100644 .github/workflows/test-build.yml create mode 100644 .github/workflows/trigger_builds.yml create mode 100644 .github/workflows/trigger_nightly_builds.yml create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conda.recipe/ginjinn-gpu-pt15_cu101_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt15_cu101_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt15_cu101_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu101_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu101_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu101_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu102_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu102_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu102_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu92_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu92_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt16_cu92_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu101_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu101_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu101_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu102_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu102_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu102_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu110_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu110_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu110_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu92_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu92_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt17_cu92_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu101_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu101_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu101_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu102_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu102_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu102_linux/post-link.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu111_linux/build.sh create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu111_linux/meta.yaml create mode 100644 conda.recipe/ginjinn-gpu-pt18_cu111_linux/post-link.sh create mode 100644 ginjinn/__init__.py create mode 100644 ginjinn/__main__.py create mode 100644 ginjinn/commandline/__init__.py create mode 100644 ginjinn/commandline/argument_parser.py create mode 100644 ginjinn/commandline/commandline_app.py create mode 100644 ginjinn/commandline/evaluate.py create mode 100644 ginjinn/commandline/info.py create mode 100644 ginjinn/commandline/main.py create mode 100644 ginjinn/commandline/new.py create mode 100644 ginjinn/commandline/predict.py create mode 100644 ginjinn/commandline/simulate.py create mode 100644 ginjinn/commandline/splitter.py create mode 100644 ginjinn/commandline/tests/test_argument_parser.py create mode 100644 ginjinn/commandline/tests/test_cmd_main.py create mode 100644 ginjinn/commandline/train.py create mode 100644 ginjinn/commandline/utils.py create mode 100644 ginjinn/data/example_data.txt create mode 100644 ginjinn/data/ginjinn_config/example_config_0.yaml create mode 100644 ginjinn/data/ginjinn_config/example_config_1.yaml create mode 100644 ginjinn/data/ginjinn_config/template_config.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_X_101_32x8d_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_X_101_32x8d_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/faster_rcnn_X_101_32x8d_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_1x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_3x.yaml create mode 100644 ginjinn/data/ginjinn_config/templates/mask_rcnn_X_101_32x8d_FPN_3x.yaml create mode 100644 ginjinn/data_reader/__init__.py create mode 100644 ginjinn/data_reader/data_error.py create mode 100644 ginjinn/data_reader/data_reader.py create mode 100644 ginjinn/data_reader/data_splitter.py create mode 100644 ginjinn/data_reader/load_datasets.py create mode 100644 ginjinn/data_reader/merge_datasets.py create mode 100644 ginjinn/data_reader/tests/test_data_splitter.py create mode 100644 ginjinn/evaluation/__init__.py create mode 100644 ginjinn/evaluation/evaluate.py create mode 100644 ginjinn/ginjinn_config/__init__.py create mode 100644 ginjinn/ginjinn_config/augmentation_config.py create mode 100644 ginjinn/ginjinn_config/config_error.py create mode 100644 ginjinn/ginjinn_config/detectron_config.py create mode 100644 ginjinn/ginjinn_config/ginjinn_config.py create mode 100644 ginjinn/ginjinn_config/input_config.py create mode 100644 ginjinn/ginjinn_config/model_config.py create mode 100644 ginjinn/ginjinn_config/options_config.py create mode 100644 ginjinn/ginjinn_config/tests/test_augmentation_configuration.py create mode 100644 ginjinn/ginjinn_config/tests/test_ginjinn_config.py create mode 100644 ginjinn/ginjinn_config/tests/test_input_configuration.py create mode 100644 ginjinn/ginjinn_config/tests/test_model_configuration.py create mode 100644 ginjinn/ginjinn_config/tests/test_options_config.py create mode 100644 ginjinn/ginjinn_config/tests/test_read_example_config.py create mode 100644 ginjinn/ginjinn_config/tests/test_train_config.py create mode 100644 ginjinn/ginjinn_config/training_config.py create mode 100644 ginjinn/predictor/__init__.py create mode 100644 ginjinn/predictor/predictors.py create mode 100644 ginjinn/predictor/tests/test_predictors.py create mode 100644 ginjinn/segmentation_refinement/LICENSE create mode 100644 ginjinn/segmentation_refinement/NOTE.txt create mode 100644 ginjinn/segmentation_refinement/__init__.py create mode 100644 ginjinn/segmentation_refinement/download.py create mode 100644 ginjinn/segmentation_refinement/eval_helper.py create mode 100644 ginjinn/segmentation_refinement/main.py create mode 100644 ginjinn/segmentation_refinement/models/__init__.py create mode 100644 ginjinn/segmentation_refinement/models/psp/__init__.py create mode 100644 ginjinn/segmentation_refinement/models/psp/extractors.py create mode 100644 ginjinn/segmentation_refinement/models/psp/pspnet.py create mode 100644 ginjinn/simulation/__init__.py create mode 100644 ginjinn/simulation/coco_utils.py create mode 100644 ginjinn/simulation/pvoc_utils.py create mode 100644 ginjinn/simulation/shapes.py create mode 100644 ginjinn/simulation/simulation.py create mode 100644 ginjinn/simulation/tests/test_simulation.py create mode 100644 ginjinn/simulation/utils.py create mode 100644 ginjinn/test_pytest.py create mode 100644 ginjinn/tests/test_main.py create mode 100644 ginjinn/trainer/__init__.py create mode 100644 ginjinn/trainer/tests/test_trainer.py create mode 100644 ginjinn/trainer/trainer.py create mode 100644 ginjinn/utils/__init__.py create mode 100644 ginjinn/utils/data_prep.py create mode 100644 ginjinn/utils/dataset_cropping.py create mode 100644 ginjinn/utils/sliding_window_merging.py create mode 100644 ginjinn/utils/utils.py create mode 100644 setup.py diff --git a/.github/workflows/pt15_cu101_manual.yml b/.github/workflows/pt15_cu101_manual.yml new file mode 100644 index 0000000..e5ce66d --- /dev/null +++ b/.github/workflows/pt15_cu101_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.5 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.5 cuda10.1 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt15_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.5.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt15_cu101_manual_nightly.yml b/.github/workflows/pt15_cu101_manual_nightly.yml new file mode 100644 index 0000000..af69a72 --- /dev/null +++ b/.github/workflows/pt15_cu101_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.5 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.5 cuda10.1 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt15_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.5.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu101_manual.yml b/.github/workflows/pt16_cu101_manual.yml new file mode 100644 index 0000000..93f8014 --- /dev/null +++ b/.github/workflows/pt16_cu101_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.6 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda10.1 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.6.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu101_manual_nightly.yml b/.github/workflows/pt16_cu101_manual_nightly.yml new file mode 100644 index 0000000..844db9d --- /dev/null +++ b/.github/workflows/pt16_cu101_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.6 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda10.1 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.6.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu102_manual.yml b/.github/workflows/pt16_cu102_manual.yml new file mode 100644 index 0000000..ed0b892 --- /dev/null +++ b/.github/workflows/pt16_cu102_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.6 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda10.2 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.6.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu102_manual_nightly.yml b/.github/workflows/pt16_cu102_manual_nightly.yml new file mode 100644 index 0000000..277c9e6 --- /dev/null +++ b/.github/workflows/pt16_cu102_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.6 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda10.2 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.6.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu92_manual.yml b/.github/workflows/pt16_cu92_manual.yml new file mode 100644 index 0000000..6b55ab4 --- /dev/null +++ b/.github/workflows/pt16_cu92_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.6 cuda9.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda9.2 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu92_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.6.0 cudatoolkit=9.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt16_cu92_manual_nightly.yml b/.github/workflows/pt16_cu92_manual_nightly.yml new file mode 100644 index 0000000..93a1479 --- /dev/null +++ b/.github/workflows/pt16_cu92_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.6 cuda9.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda9.2 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt16_cu92_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.6.0 cudatoolkit=9.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu101_manual.yml b/.github/workflows/pt17_cu101_manual.yml new file mode 100644 index 0000000..ec7a71d --- /dev/null +++ b/.github/workflows/pt17_cu101_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.7 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda10.1 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.7.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu101_manual_nightly.yml b/.github/workflows/pt17_cu101_manual_nightly.yml new file mode 100644 index 0000000..438f865 --- /dev/null +++ b/.github/workflows/pt17_cu101_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.7 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda10.1 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.7.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu102_manual.yml b/.github/workflows/pt17_cu102_manual.yml new file mode 100644 index 0000000..1bbb264 --- /dev/null +++ b/.github/workflows/pt17_cu102_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.7 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda10.2 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.7.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu102_manual_nightly.yml b/.github/workflows/pt17_cu102_manual_nightly.yml new file mode 100644 index 0000000..18e91b2 --- /dev/null +++ b/.github/workflows/pt17_cu102_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.7 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda10.2 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.7.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu110_manual.yml b/.github/workflows/pt17_cu110_manual.yml new file mode 100644 index 0000000..2fd3b67 --- /dev/null +++ b/.github/workflows/pt17_cu110_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.7 cuda11.0 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda11.0 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu110_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.7.0 cudatoolkit=11.0 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu110_manual_nightly.yml b/.github/workflows/pt17_cu110_manual_nightly.yml new file mode 100644 index 0000000..ec6b9bf --- /dev/null +++ b/.github/workflows/pt17_cu110_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.7 cuda11.0 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.7 cuda11.0 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu110_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.7.0 cudatoolkit=11.0 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu92_manual.yml b/.github/workflows/pt17_cu92_manual.yml new file mode 100644 index 0000000..71a9631 --- /dev/null +++ b/.github/workflows/pt17_cu92_manual.yml @@ -0,0 +1,58 @@ +name: Manual Build pytorch1.7 cuda9.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda9.2 build' + workflow_run: + workflows: ['Trigger Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu92_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.7.0 cudatoolkit=9.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt17_cu92_manual_nightly.yml b/.github/workflows/pt17_cu92_manual_nightly.yml new file mode 100644 index 0000000..40952da --- /dev/null +++ b/.github/workflows/pt17_cu92_manual_nightly.yml @@ -0,0 +1,59 @@ +name: Manual Nightly Build pytorch1.7 cuda9.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.6 cuda9.2 build' + workflow_run: + workflows: ['Trigger Nightly Builds'] + types: + - completed +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt17_cu92_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.7.0 cudatoolkit=9.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu101_manual.yml b/.github/workflows/pt18_cu101_manual.yml new file mode 100644 index 0000000..99af844 --- /dev/null +++ b/.github/workflows/pt18_cu101_manual.yml @@ -0,0 +1,55 @@ +name: Manual Build pytorch1.8 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda10.1 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.8.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu101_manual_nightly.yml b/.github/workflows/pt18_cu101_manual_nightly.yml new file mode 100644 index 0000000..11f9fb9 --- /dev/null +++ b/.github/workflows/pt18_cu101_manual_nightly.yml @@ -0,0 +1,56 @@ +name: Manual Nightly Build pytorch1.8 cuda10.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda10.1 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.8.0 cudatoolkit=10.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu102_manual.yml b/.github/workflows/pt18_cu102_manual.yml new file mode 100644 index 0000000..94f63f8 --- /dev/null +++ b/.github/workflows/pt18_cu102_manual.yml @@ -0,0 +1,55 @@ +name: Manual Build pytorch1.8 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda10.2 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.8.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu102_manual_nightly.yml b/.github/workflows/pt18_cu102_manual_nightly.yml new file mode 100644 index 0000000..d1e08ba --- /dev/null +++ b/.github/workflows/pt18_cu102_manual_nightly.yml @@ -0,0 +1,56 @@ +name: Manual Nightly Build pytorch1.8 cuda10.2 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda10.2 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu102_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.8.0 cudatoolkit=10.2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu111_manual.yml b/.github/workflows/pt18_cu111_manual.yml new file mode 100644 index 0000000..62247a7 --- /dev/null +++ b/.github/workflows/pt18_cu111_manual.yml @@ -0,0 +1,55 @@ +name: Manual Build pytorch1.8 cuda11.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda11.1 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu111_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler ginjinn2 pytorch=1.8.0 cudatoolkit=11.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/pt18_cu111_manual_nightly.yml b/.github/workflows/pt18_cu111_manual_nightly.yml new file mode 100644 index 0000000..f68b5d6 --- /dev/null +++ b/.github/workflows/pt18_cu111_manual_nightly.yml @@ -0,0 +1,56 @@ +name: Manual Nightly Build pytorch1.8 cuda11.1 + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Test pytorch1.8 cuda11.1 build' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt18_cu111_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 pytorch=1.8.0 cudatoolkit=11.1 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml new file mode 100644 index 0000000..8b99b31 --- /dev/null +++ b/.github/workflows/test-build.yml @@ -0,0 +1,60 @@ +name: Test Build + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Build GinJinn2, publish and run tests.' + push: + branches: [ main, development ] + pull_request: + branches: [ main, development ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8] + ginjinn-distribution: [ginjinn-gpu-pt15_cu101_linux] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + NIGHTLY: true + name: Python ${{ matrix.python-version }} example + steps: + - uses: actions/checkout@v2 + - name: Setup conda + uses: s-weigand/setup-conda@v1 + with: + update-conda: true + python-version: ${{ matrix.python-version }} + conda-channels: anaconda, conda-forge, pytorch + - run: conda --version + - run: which python + - name: Install dependencies + run: | + conda install pytest conda-build anaconda-client pip pytest-cov + - name: Build and publish GinJinn + run: | + conda config --set anaconda_upload yes + conda-build conda.recipe/${{ matrix.ginjinn-distribution }} --token ${{ secrets.ANACONDA_TOKEN }} --user AGOberprieler --variants "{'python': ['${{ matrix.python-version }}']}" --label nightly + - name: Install GinJinn from Anaconda cloud + run: conda install -c conda-forge -c pytorch -c agoberprieler/label/nightly ginjinn2 + - name: Test with pytest + run: | + pytest --cov=./ginjinn/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml new file mode 100644 index 0000000..8f75f97 --- /dev/null +++ b/.github/workflows/trigger_builds.yml @@ -0,0 +1,21 @@ +name: Trigger Builds + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Trigger Builds' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Release artifact + run: echo "Builds Triggered" \ No newline at end of file diff --git a/.github/workflows/trigger_nightly_builds.yml b/.github/workflows/trigger_nightly_builds.yml new file mode 100644 index 0000000..d7f85a4 --- /dev/null +++ b/.github/workflows/trigger_nightly_builds.yml @@ -0,0 +1,21 @@ +name: Trigger Nightly Builds + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + tags: + description: 'Trigger Nightly Builds' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Release artifact + run: echo "Builds Triggered" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba37326 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# VSCode +.vscode/ + +# conda-build +.empty + +# local tests +local_tests + +# vscode +.vscode diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..a3b2ac8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,611 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=test_* + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=pylint.extensions.docparams + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _, + a, + b, + c, + x, + y, + w, + h, + r, + ax, + x0, + x1, + y0, + y1, + dx, + dy, + cl + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6905e83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 AGOberprieler + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b001e56 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# GinJinn_development + +![GitHub](https://img.shields.io/github/license/agoberprieler/ginjinn_development) + +GinJinn2 provides a collection of command-line tools for bounding-box object detection and instance segmentation based on [Detectron2](https://github.com/facebookresearch/detectron2). +Besides providing a convenient interface to the latter, GinJinn2 offers several utility functions to facilitate building custom pipelines. + +## Installation +### Requirements +### Installation via Conda +It is recommended to install GinJinn via [Conda](https://docs.conda.io/en/latest/), an open-source package management system for Python and R, which also includes facilities for environment management system. See the [official installation guide](https://conda.io/projects/conda/en/latest/user-guide/install/linux.html) for further information. + +To install Conda, run the following commands in your Linux terminal: +```bash +wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh +bash Miniconda3-latest-Linux-x86_64.sh +``` + +## Usage +Make sure to activate your Conda environment via `conda activate MY_ENV_NAME` prior to running any ginjinn command. + + +`ginjinn` and all of its subcommands provide help pages, which can be displayed using the argument `-h` or `--help`, e.g. + +- `ginjinn -h` (get list of all essential GinJinn commands) +- `ginjinn utils -h` (get list of GinJinn's additional utilities) +- `ginjinn utils crop -h` (get usage information for the cropping utility) + +The help pages briefly describe basic functionality and command-specific arguments. + + +### Dataset formats + +A (labeled) input dataset should consist of a single image directory containing JPG images at its top level and accompanying annotations. GinJinn2 supports two common annotation formats, [COCO's data format](https://cocodataset.org/#format-data) (one JSON file per dataset), which is also used as output format, and XML files as used by [PASCAL VOC](http://host.robots.ox.ac.uk/pascal/VOC/) (one file per image). The latter, however, is only supported for bounding-box object detection. + +Although not mandatory, it is recommended to place image directory and annotations in a common directory to enable more compact command invocations. If datasets are structured as shown below, the user does not have specify the image directory explicitely. Note that the file names are arbitrarily chosen. + +- COCO + + ``` + data + ├── annotations.json + └── images + ├── 1.jpg + ├── 2.jpg + └── ... + ``` + +- Pascal VOC + + ``` + data + ├── annotations + │ ├── 1.xml + │ ├── 2.xml + │ └── ... + └── images + ├── 1.jpg + ├── 2.jpg + └── ... + ``` + +In case of nested image directories, `ginjinn utils flatten` helps to convert datasets to an accepted format. + +### Train-val-test split +In addition to the dataset for training the model, it is advisable to provide a validation dataset, which can be used to optimize (hyper)parameters and to detect overfitting. A further test dataset, if available, allows to obtain an unbiased evaluation of the final, trained model. + +`ginjinn split` can be used to partition a single dataset such that each image along with its annotated objects is assigned to only one of two or three sub-datasets ("train", "val", "test"). Aiming at a balanced split across different object categories, a simple heuristic is used to propose dataset partitions. The generated output has the following structure: + +- COCO + + ``` + data + ├── train + │ ├── annotations.json + │ └── images + ├── val + │ ├── annotations.json + │ └── images + └── test + ├── annotations.json + └── images + ``` + +- Pascal VOC + + ``` + data + ├── train + │ ├── annotations + │ └── images + ├── val + │ ├── annotations + │ └── images + └── test + ├── annotations + └── images + ``` + +### Basic workflow + +1. `ginjinn new` + + This command generates a new project directory, which is required for training, evaluation, and prediction. Initially, it only contains an empty output folder and the GinJinn2 configuration file (“ginjinn_config.yaml”), a simple, formatted text file for storing various settings, which can be edited by the user for customization. When executing certain GinJinn2 commands, further data may be written to the project directory. To avoid inconsistencies, it is strongly recommended to keep the configuration file fixed throughout subsequent steps of the analysis. + + The `-t`/`--template` and `-d`/`--data_dir` options allow to automatically specify various settings such that a valid configuration can be created without manually editing the configuration file. + +2. `ginjinn train` + + This command trains the model and simultaneously evaluates it using the validation dataset, if available. Checkpoints are saved periodically at predefined intervalls. While the training is running, its progress can be most easily monitored via the "outputs/metrics.pdf" file in your project directory. + If the training has finished or in case of interruption, it can be resumed with `-r`/`--resume`. The number of iterations as specified in the configuration file can be overwritten with `-n`/`--n_iter`. + +3. `ginjinn evaluate` + + This command calculates [COCO evaluation metrics](https://cocodataset.org/#detection-eval) for a trained model using the test dataset. + +4. `ginjinn predict` + + This command uses a trained model to predict objects for new, unlabeled images. It provides several optional types of output: a COCO annotation file, object visualization on the original images, and cropped images (segmentation masks) for each predicted object. + + +Concrete workflows including more complex examples are described in XYZ. + +## License + +GinJinn2 is released under the [Apache 2.0 license](LICENSE). + +## Citation + +XYZ diff --git a/conda.recipe/ginjinn-gpu-pt15_cu101_linux/build.sh b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt15_cu101_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/meta.yaml new file mode 100644 index 0000000..8eaa2fc --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/meta.yaml @@ -0,0 +1,40 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.5.0 + - torchvision==0.6.0 + - cudatoolkit=10.1 + - pip + + run: + - python {{ python }} + - pytorch==1.5.0 + - torchvision==0.6.0 + - cudatoolkit=10.1 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt15_cu101_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. diff --git a/conda.recipe/ginjinn-gpu-pt15_cu101_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/post-link.sh new file mode 100644 index 0000000..0adb592 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt15_cu101_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu101/torch1.5/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu101_linux/build.sh b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu101_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/meta.yaml new file mode 100644 index 0000000..2c26d51 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=10.1 + - pip + + run: + - python {{ python }} + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=10.1 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt16_cu101_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu101_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/post-link.sh new file mode 100644 index 0000000..c944e65 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu101_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu101/torch1.6/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu102_linux/build.sh b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu102_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/meta.yaml new file mode 100644 index 0000000..3d8d598 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/meta.yaml @@ -0,0 +1,43 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=10.2 + - pip + + run: + - python {{ python }} + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=10.2 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt16_cu102_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu102_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/post-link.sh new file mode 100644 index 0000000..d415d87 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu102_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu102/torch1.6/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu92_linux/build.sh b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu92_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/meta.yaml new file mode 100644 index 0000000..e3484de --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/meta.yaml @@ -0,0 +1,41 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=9.2 + - pip + + run: + - python {{ python }} + - pytorch==1.6.0 + - torchvision==0.7.0 + - cudatoolkit=9.2 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt16_cu92_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt16_cu92_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/post-link.sh new file mode 100644 index 0000000..680f321 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt16_cu92_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu92/torch1.6/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu101_linux/build.sh b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu101_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/meta.yaml new file mode 100644 index 0000000..52aaf87 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=10.1 + - pip + + run: + - python {{ python }} + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=10.1 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt17_cu101_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu101_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/post-link.sh new file mode 100644 index 0000000..2dc7276 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu101_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu101/torch1.7/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu102_linux/build.sh b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu102_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/meta.yaml new file mode 100644 index 0000000..eec401a --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=10.2 + - pip + + run: + - python {{ python }} + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=10.2 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt17_cu102_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu102_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/post-link.sh new file mode 100644 index 0000000..458adec --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu102_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu102/torch1.7/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu110_linux/build.sh b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu110_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/meta.yaml new file mode 100644 index 0000000..2bcf6b0 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=11.0 + - pip + + run: + - python {{ python }} + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=11.0 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt17_cu110_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu110_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/post-link.sh new file mode 100644 index 0000000..550cf0f --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu110_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu110/torch1.7/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu92_linux/build.sh b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu92_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/meta.yaml new file mode 100644 index 0000000..37ffad9 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/meta.yaml @@ -0,0 +1,41 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=9.2 + - pip + + run: + - python {{ python }} + - pytorch==1.7.0 + - torchvision==0.8.0 + - cudatoolkit=9.2 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt17_cu92_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt17_cu92_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/post-link.sh new file mode 100644 index 0000000..c727fe5 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt17_cu92_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu92/torch1.7/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu101_linux/build.sh b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu101_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/meta.yaml new file mode 100644 index 0000000..0cd35de --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=10.1 + - pip + + run: + - python {{ python }} + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=10.1 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt18_cu101_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu101_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/post-link.sh new file mode 100644 index 0000000..040b488 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu101_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu101/torch1.8/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu102_linux/build.sh b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu102_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/meta.yaml new file mode 100644 index 0000000..0d4a9ff --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=10.2 + - pip + + run: + - python {{ python }} + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=10.2 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt18_cu102_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. diff --git a/conda.recipe/ginjinn-gpu-pt18_cu102_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/post-link.sh new file mode 100644 index 0000000..094c663 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu102_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu102/torch1.8/index.html \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu111_linux/build.sh b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/build.sh new file mode 100644 index 0000000..fec5047 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/build.sh @@ -0,0 +1 @@ +$PYTHON setup.py install # Python command to install the script. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu111_linux/meta.yaml b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/meta.yaml new file mode 100644 index 0000000..d8e435e --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ginjinn2 + version: {{ environ.get('GIT_DESCRIBE_TAG', 'default').lstrip('v') }} + +source: + + git_url: https://github.com/AGOberprieler/GinJinn2 + +requirements: + build: + - python {{ python }} + - setuptools + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=11.1 + - pip + + run: + - python {{ python }} + - pytorch==1.8.0 + - torchvision==0.9.0 + - cudatoolkit=11.1 + - opencv + - scikit-image + - pandas + - pycocotools + - mock + - imantics + - scikit-learn + +build: + string: pt18_cu111_py{{ python | replace(".", "") }}{{ '_nightly' if environ.get('NIGHTLY') else '' }} + skip: true # [not linux] +entry_points: + - ginjinn = ginjinn.commandline.main:main + + + +about: + home: https://github.com/AGOberprieler/GinJinn2 + license: Apache-2.0 + summary: An object detection framework for biologists. \ No newline at end of file diff --git a/conda.recipe/ginjinn-gpu-pt18_cu111_linux/post-link.sh b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/post-link.sh new file mode 100644 index 0000000..30ae940 --- /dev/null +++ b/conda.recipe/ginjinn-gpu-pt18_cu111_linux/post-link.sh @@ -0,0 +1 @@ +python -m pip install detectron2 -f https://dl.fbaipublicfiles.com/detectron2/wheels/cu111/torch1.8/index.html \ No newline at end of file diff --git a/ginjinn/__init__.py b/ginjinn/__init__.py new file mode 100644 index 0000000..eade861 --- /dev/null +++ b/ginjinn/__init__.py @@ -0,0 +1,25 @@ +''' GinJinn + +Summary +------- +GinJinn is an object detection framework for the detection of structures in +digitized herbarium specimens. +GinJinn includes bounding-box detection and segmentation functionality using +state of the art computer vision models. +Further, it includes functionality for loading und exporting data in most +of the common data formats. + +References +---------- +Ott, T., C. Palm, R. Vogt, and C. Oberprieler. 2020. GinJinn: An object-detection +pipeline for automated feature extraction from herbarium specimens. +Applications in Plant Sciences 2020 8(6). +''' +__version__ = '0.0.1' + +import pkg_resources + +# an example for loading package data +example_data_path = pkg_resources.resource_filename('ginjinn', 'data/example_data.txt') +with open(example_data_path) as f: + example_data = f.read() diff --git a/ginjinn/__main__.py b/ginjinn/__main__.py new file mode 100644 index 0000000..0cf7f4c --- /dev/null +++ b/ginjinn/__main__.py @@ -0,0 +1,7 @@ +''' GinJinn main +''' + +from ginjinn import commandline + +if __name__ == "__main__": + commandline.main() diff --git a/ginjinn/commandline/__init__.py b/ginjinn/commandline/__init__.py new file mode 100644 index 0000000..0cf88d5 --- /dev/null +++ b/ginjinn/commandline/__init__.py @@ -0,0 +1,5 @@ +''' Module for handling commandline interactions +''' + +from .argument_parser import GinjinnArgumentParser +from .main import main diff --git a/ginjinn/commandline/argument_parser.py b/ginjinn/commandline/argument_parser.py new file mode 100644 index 0000000..e3ba34e --- /dev/null +++ b/ginjinn/commandline/argument_parser.py @@ -0,0 +1,1654 @@ +''' Module for setting up and handling argument parsers +''' + +import argparse +import glob +from os.path import basename, join +import pkg_resources + +def _setup_new_parser(subparsers): + '''_setup_new_parser + + Setup parser for the ginjinn new subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the new subcommand. + ''' + + parser = subparsers.add_parser( + 'new', + help = ''' + Create a new GinJinn project. + ''', + description = ''' + Create a new GinJinn project. + ''', + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + 'project_dir', + type = str, + help = ''' + Path to new GinJinn project directory. + ''' + ) + + template_dir = pkg_resources.resource_filename( + 'ginjinn', 'data/ginjinn_config/templates', + ) + template_files = glob.glob(join(template_dir, '*.yaml')) + templates = sorted([basename(t_f) for t_f in template_files]) + templates = [t for t in templates if not t.startswith('adv_')] + templates_string = '\n'.join(f'- {t}' for t in templates) + parser.add_argument( + '-t', '--template', + type = str, + help = f''' +Model template to initialize the new GinJinn project with. +Faster RCNN models are bounding-box detection models, while +Mask RCNN models are segmentation models. + +Available templates are: +{templates_string} + +(default: "faster_rcnn_R_50_FPN_3x.yaml") + ''', + choices=templates, + default='faster_rcnn_R_50_FPN_3x.yaml', + metavar='TEMPLATE' + ) + + parser.add_argument( + '-a', '--advanced', + dest='advanced', + action='store_true', + help='Generate config exposing advanced options.' + ) + parser.set_defaults(advanced=False) + + parser.add_argument( + '-d', '--data_dir', + type=str, + default=None, + help=''' + Data directory to initialize the project config for. Can either be + either the path to a COCO/PVOC dataset directory, or the path to a split + directory as built by "ginjinn split". + ''' + ) + + return parser + +def _setup_train_parser(subparsers): + '''_setup_train_parser + + Setup parser for the ginjinn train subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the train subcommand. + ''' + + parser = subparsers.add_parser( + 'train', + help = ''' + Train a GinJinn model. + ''', + description = ''' + Train a GinJinn model. + ''' + ) + parser.add_argument( + 'project_dir', + type = str, + help = ''' + Path to GinJinn project directory. + ''' + ) + # parser.add_argument( + # '-nr', '--no_resume', + # type = bool, + # help = ''' + # Do not resume training. If this option is set, training will + # start from scratch, discarding previous training checkpoints + # PERMANENTLY. + # ''', + # # action='store_true', + # default=None, + # ) + + parser.add_argument( + '-n', '--n_iter', + type = int, + help = 'Number of iterations.', + default = None, + ) + + parser.add_argument('-r', '--resume', dest='resume', action='store_true') + parser.add_argument('-nr', '--no-resume', dest='resume', action='store_false') + parser.set_defaults(resume=None) + + parser.add_argument( + '-f', '--force', + dest='force', + action='store_true', + help='Force removal of existing outputs when resume is set to False.' + ) + parser.set_defaults(force=False) + + return parser + +def _setup_evaluate_parser(subparsers): + '''_setup_evaluate_parser + + Setup parser for the ginjinn evaluate subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the evaluate subcommand. + ''' + + # TODO: implement + + parser = subparsers.add_parser( + 'evaluate', + aliases=['eval'], + help = ''' + Evaluate a trained GinJinn model. + ''', + description = ''' + Evaluate a trained GinJinn model. + ''' + ) + parser.add_argument( + 'project_dir', + type = str, + help = ''' + Path to GinJinn project directory. + ''' + ) + + optional = parser.add_argument_group('optional arguments') + optional.add_argument( + '-c', '--checkpoint', + type = str, + help = ''' + Checkpoint file name. By default the most recent checkpoint + (model_final.pth) will be used. + ''', + default = "model_final.pth", + ) + + return parser + +def _setup_predict_parser(subparsers): + '''_setup_predict_parser + + Setup parser for the ginjinn predict subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the predict subcommand. + ''' + + # TODO: implement + + parser = subparsers.add_parser( + 'predict', + help = ''' + Predict from a trained GinJinn model. + ''', + description = ''' + Predict from a trained GinJinn model. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + 'project_dir', + type = str, + help = ''' + Path to GinJinn project directory. + ''' + ) + + # Required + required = parser.add_argument_group('required arguments') + required.add_argument( + '-i', '--image_path', + type = str, + help = ''' + Either path to an image directory or to a single image. + ''', + required=True, + ) + + # Optional + optional = parser.add_argument_group('optional arguments') + optional.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Output directory. If None, output will be written to + "/prediction". + ''', + default = None, + ) + + optional.add_argument( + '-c', '--checkpoint', + type = str, + help = ''' + Checkpoint file name. By default the most recent checkpoint + (model_final.pth) will be used. + ''', + default = "model_final.pth", + ) + + optional.add_argument( + '-t', '--threshold', + type = float, + help = ''' + Prediction threshold. Only predictions with scores >= threshold are saved. + ''', + default = 0.8 + ) + + optional.add_argument( + '-p', '--padding', + type = int, + help = ''' + Padding for cropping bounding boxes. + ''', + default = 0 + ) + + required.add_argument( + '-s', '--output_types', + help = ''' + Output types. + ''', + choices=['COCO', 'cropped', 'visualization'], + nargs='+', + action='append', + default=['COCO'], + ) + + optional.add_argument( + '-r', '--seg_refinement', + dest = 'seg_refinement', + action = 'store_true', + help = ''' + Apply segmentation refinement. + ''' + ) + parser.set_defaults(seg_refinement = False) + + optional.add_argument( + '-m', '--refinement_method', + help = ''' + Refinement method. Either "fast" or "full". + ''', + choices=['fast', 'full'], + default='full', + ) + + optional.add_argument( + '-d', '--device', + help = ''' + Refinement device. + ''', + type=str, + default='cuda:0', + ) + + return parser + +def _setup_split_parser(subparsers): + '''_setup_split_parser + + Setup parser for the ginjinn split subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the split subcommand. + ''' + + parser = subparsers.add_parser( + 'split', + help = ''' + Split dataset (images and annotations) into test, train, and optionally + evaluation datasets. + ''', + description = ''' + Split dataset (images and annotations) into test, train, and optionally + evaluation datasets. + ''' + ) + required_parser = parser.add_argument_group('required named arguments') + required_parser.add_argument( + '-a', '--annotation_path', + type = str, + help = ''' + Path to directory containing annotations (PVOC) or path to an annotation + JSON file (COCO). + ''', + required = True, + ) + required_parser.add_argument( + '-o', '--output_dir', + type = str, + help = ''' + Path to output directory. Splits will be written to output_dir/train, + output_dir/test, and output_dir/eval, respectively. The output directory + will be created, if it does not exist. + ''', + required = True, + ) + required_parser.add_argument( + '-d', '--task', + type = str, + choices = [ + 'instance-segmentation', 'bbox-detection' + ], + help = ''' + Task, which the dataset will be used for. + ''', + required = True, + ) + optional_parser = parser.add_argument_group('optional arguments') + optional_parser.add_argument( + '-i', '--image_dir', + type = str, + help = ''' + Path to directory containing images. By default, GinJinn searches for + a sibling directory to "annotation_path" called "images". + ''', + default=None, + ) + optional_parser.add_argument( + '-k', '--ann_type', + type = str, + choices = ['auto', 'COCO', 'PVOC'], + help = ''' + Annotation type. If 'auto', annotation type will be inferred. + ''', + default='auto', + ) + optional_parser.add_argument( + '-t', '--test_fraction', + type = float, + help = ''' + Fraction of the dataset to use for testing. (Default: 0.2) + ''', + default = 0.2, + ) + optional_parser.add_argument( + '-v', '--validation_fraction', + type = float, + help = ''' + Fraction of the dataset to use for validation while training. (Default: 0.2) + ''', + default = 0.2, + ) + + return parser + +def _setup_info_parser(subparsers): + '''_setup_info_parser + + Setup parser for the ginjinn info subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the info subcommand. + ''' + + info_parser = subparsers.add_parser( + 'info', + help = ''' + Print dataset info. + ''', + description = ''' + Print dataset info. + ''', + ) + + # required + info_parser_required = info_parser.add_argument_group('required arguments') + info_parser_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to COCO annotation file (JSON) or PVCO annotation directory. + ''', + required=True, + ) + info_parser_optional = info_parser.add_argument_group('optional arguments') + info_parser_optional.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Directory containing the annotated images. + ''', + default = None, + ) + info_parser_optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type. If 'auto', annotation type will be inferred. + ''', + choices = ['auto', 'COCO', 'PVOC'], + default = 'auto' + ) + + return info_parser + +def _setup_simulate_parser(subparsers): + '''_setup_simulate_parser + + Setup parser for the ginjinn simulate subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the simulate subcommand. + ''' + + # TODO: implement + + parser = subparsers.add_parser( + 'simulate', + help = ''' + Simulate datasets. + ''', + description = ''' + Simulate datasets. + ''', + ) + simulate_parsers = parser.add_subparsers( + dest='simulate_subcommand', + help='Types of simulations.' + ) + + # == shapes + shapes_parser = simulate_parsers.add_parser( + 'shapes', + help = ''' + Simulate a simple segmentation dataset with COCO annotations, + or a simple bounding-box dataset with PVOC annotations, + containing two classes: circles and triangles. + ''', + description = ''' + Simulate a simple segmentation dataset with COCO annotations, + or a simple bounding-box dataset with PVOC annotations, + containing two classes: circles and triangles. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + required = shapes_parser.add_argument_group('required arguments') + required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the simulated data should be written to. + ''', + required=True, + ) + + optional = shapes_parser.add_argument_group('optional arguments') + optional.add_argument( + '-a', '--ann_type', + type = str, + help = ''' + Type of annotations to simulate. + ''', + choices=['COCO', 'PVOC'], + default='COCO', + ) + optional.add_argument( + '-n', '--n_images', + type = int, + help = ''' + Number of images to simulate. + ''', + default=100, + ) + optional.add_argument( + '-w0', '--min_w', + type = int, + help = ''' + Minimum image width. + ''', + default=400, + ) + optional.add_argument( + '-w1', '--max_w', + type = int, + help = ''' + Maximum image width. + ''', + default=400, + ) + optional.add_argument( + '-h0', '--min_h', + type = int, + help = ''' + Minimum image height. + ''', + default=400, + ) + optional.add_argument( + '-h1', '--max_h', + type = int, + help = ''' + Maximum image height. + ''', + default=400, + ) + optional.add_argument( + '-n0', '--min_n_shapes', + type = int, + help = ''' + Minimum number of shapes per image. + ''', + default=1, + ) + optional.add_argument( + '-n1', '--max_n_shapes', + type = int, + help = ''' + Maximum number of shapes per image. + ''', + default=4, + ) + optional.add_argument( + '-t', '--triangle_prob', + type = float, + help = ''' + Probability of generating a triangle. Default is 0.5, meaning that + triangles and circle are equally represented. + ''', + default=0.5, + ) + optional.add_argument( + '-ccol', '--circle_col', + type = str, + help = ''' + Mean circle color as Hex color code. + ''', + default='#C87D7D', + ) + optional.add_argument( + '-tcol', '--triangle_col', + type = str, + help = ''' + Mean triangle color as Hex color code. + ''', + default='#7DC87D', + ) + optional.add_argument( + '-cvar', '--color_variance', + type = float, + help = ''' + Variance around mean shape colors. + ''', + default=0.15, + ) + optional.add_argument( + '-mnr', '--min_shape_radius', + type = float, + help = ''' + Minimum shape radius. + ''', + default=25.0, + ) + optional.add_argument( + '-mxr', '--max_shape_radius', + type = float, + help = ''' + Maximum shape radius. + ''', + default=75.0, + ) + optional.add_argument( + '-mna', '--min_shape_angle', + type = float, + help = ''' + Minimum shape rotation in degrees. + ''', + default=0.0, + ) + optional.add_argument( + '-mxa', '--max_shape_angle', + type = float, + help = ''' + Maximum shape rotation in degrees. + ''', + default=60.0, + ) + optional.add_argument( + '-b', '--noise', + type = float, + help = ''' + Amount of noise to add. + ''', + default=0.005, + ) + + # == + # ... further simulations ... + # == + + return parser + +def _setup_utils_parser(subparsers): + '''_setup_utils_parser + + Setup parser for the ginjinn utils subcommand. + + Parameters + ---------- + subparsers + An object returned by argparse.ArgumentParser.add_subparsers() + + Returns + ------- + parser + An argparse ArgumentParser, registered for the utils subcommand. + ''' + + parser = subparsers.add_parser( + 'utils', + help = ''' + Utility commands. + ''', + description = ''' + Utility commands. + ''', + ) + + utils_parsers = parser.add_subparsers( + dest='utils_subcommand', + help='Utility commands.', + ) + utils_parsers.required = True + + # == cleanup + cleanup_parser = utils_parsers.add_parser( + 'cleanup', + help = ''' + Cleanup GinJinn project directory, removing the outputs directory and evaluation an training results. + ''', + description = ''' + Cleanup GinJinn project directory, removing the outputs directory and evaluation an training results. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + cleanup_parser.add_argument( + 'project_dir', + type = str, + help = ''' + GinJinn project directory to be cleaned up. + ''', + ) + + # == merge + merge_parser = utils_parsers.add_parser( + 'merge', + help = ''' + Merge multiple data sets. + ''', + description = ''' + Merge multiple data sets. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + required = merge_parser.add_argument_group('required arguments') + + required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the merged data set should be written to. + ''', + required=True, + ) + + required.add_argument( + '-i', '--image_dir', + type = str, + help = ''' + Path to a single image directory. + ''', + required=True, + nargs='+', + action='append', + ) + + required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to a single annotation file (COCO) or annotations directory (PVOC). + ''', + required=True, + nargs='+', + action='append', + ) + + # optional + optional = merge_parser.add_argument_group('optional arguments') + optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type of the data set. + ''', + choices=['COCO', 'PVOC'], + default='COCO', + ) + + optional.add_argument( + '-l', '--link_images', + dest = 'link_images', + action = 'store_true', + help = ''' + Create hard links instead of copying images. + ''' + ) + parser.set_defaults(link_images = False) + + # == flatten + flatten_parser = utils_parsers.add_parser( + 'flatten', + help = ''' + Flatten a COCO data set (move all images in same directory). + ''', + description = ''' + Flatten a COCO data set (move all images in same directory). + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + flatten_required = flatten_parser.add_argument_group('required arguments') + + flatten_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the flattened data set should be written to. + ''', + required=True, + ) + + flatten_required.add_argument( + '-i', '--image_root_dir', + type = str, + help = ''' + Path to root image directory. For COCO this is generally the "images" directory + within the COCO data set directory. + ''', + required=True, + ) + + flatten_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to the JSON annotation file. + ''', + required=True, + ) + + # optional + flatten_optional = flatten_parser.add_argument_group('optional arguments') + flatten_optional.add_argument( + '-s', '--seperator', + type = str, + help = ''' + Seperator for the image path flattening. + ''', + default='~', + ) + flatten_optional.add_argument( + '-c', '--custom_id', + dest = 'custom_id', + action = 'store_true', + help = ''' + Replace image file names with a custom id. An ID mapping file + will be written if this option is set. + ''' + ) + parser.set_defaults(custom_id = False) + + flatten_optional.add_argument( + '-x', '--annotated_only', + dest = 'annotated_only', + action = 'store_true', + help = ''' + Whether only annotated images should be kept in the data set. + ''' + ) + parser.set_defaults(annotated_only = False) + + # == crop + crop_parser = utils_parsers.add_parser( + 'crop', + help = ''' + Crop COCO dataset bounding boxes as single images. + This is useful for multi-step models, e.g. training a bbox model + and a segmentation model on the cropped bboxes. + ''', + description = ''' + Crop COCO dataset bounding boxes as single images. + This is useful for multi-step models, e.g. training a bbox model + and a segmentation model on the cropped bboxes. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + crop_required = crop_parser.add_argument_group('required arguments') + + crop_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the cropped data set should be written to. + ''', + required=True, + ) + crop_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to the JSON annotation file. + ''', + required=True, + ) + + # optional + crop_optional = crop_parser.add_argument_group('optional arguments') + crop_optional.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Path to image directory. By default, will be inferred. + ''', + default=None, + ) + crop_optional.add_argument( + '-p', '--padding', + type = int, + help = ''' + Padding for bbox cropping. + ''', + default=5, + ) + crop_optional.add_argument( + '-t', '--type', + type=str, + help=''' + Cropping type. When "segmentation" (default) is selected, + only bboxes with a corresponding segmentation will be cropped. + ''', + choices=['segmentation', 'bbox'], + default='segmentation' + ) + + # == sliding_window + sliding_window_parser = utils_parsers.add_parser( + 'sliding_window', + help = ''' + Crop images and corresponding annotation into sliding windows. + ''', + description = ''' + Crop images and corresponding annotation into sliding windows. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + sliding_window_required = sliding_window_parser.add_argument_group('required arguments') + + sliding_window_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the sliding-window cropped data set should be written to. + ''', + required=True, + ) + sliding_window_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to the JSON annotation file for COCO annotations or + path to a directory containing XML annotations for PVOC annotations. + ''', + required=True, + ) + + # optional + sliding_window_optional = sliding_window_parser.add_argument_group('optional arguments') + sliding_window_optional.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Path to image directory. By default, will be inferred. + ''', + default = None, + ) + sliding_window_optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type. If "auto", will be inferred. + ''', + choices = ['auto', 'COCO', 'PVOC'], + default = 'auto', + ) + sliding_window_optional.add_argument( + '-s', '--window_size', + type = int, + nargs = '+', + help = ''' + Sliding window size in pixel. + If one argument is passed, quadratic windows of window_size will be generated. + If two arguments are passed, they are interpreted as window width and height, respectively. + + "-s 500", for example, crops sliding windows of size 500*500 (w*h), while "-s 500 300" crops + windows of size 500*300. + ''', + default=[1000], + ) + sliding_window_optional.add_argument( + '-p', '--overlap', + type = int, + nargs = '+', + help = ''' + Overlap between sliding windows. + If one argument is passed, the same overlap is used in horizontal and vertical direction. + If two arguments are passed, they are interpreted as overlap in horizontal and + vertical, respectively. + ''', + default=[200], + ) + sliding_window_optional.add_argument( + '-k', '--task', + choices = [ + 'instance-segmentation', 'bbox-detection' + ], + help = ''' + Task, which the dataset will be used for. Only applies to COCO + datasets. + ''', + default = 'instance-segmentation', + ) + sliding_window_optional.add_argument( + '-m', '--img_id', + type = int, + help = ''' + Starting image ID for newly generated image annotations. + ''', + default=1, + ) + sliding_window_optional.add_argument( + '-b', '--obj_id', + type = int, + help = ''' + Starting object ID for newly generated object annotations. + ''', + default=1, + ) + sliding_window_optional.add_argument( + '-r', '--remove_empty', + dest = 'remove_empty', + action = 'store_true', + help = ''' + If this flag is set, cropped images without object annotation will + not be saved. + ''' + ) + parser.set_defaults(remove_empty = False) + sliding_window_optional.add_argument( + '-c', '--remove_incomplete', + dest = 'remove_incomplete', + action = 'store_true', + help = ''' + If this flag is set, object annotations that are touched (trimmed) + by a sliding-window edge are removed from the corresponding sliding-window images. + ''' + ) + parser.set_defaults(remove_incomplete = False) + + # == sw_image + sw_image_parser = utils_parsers.add_parser( + 'sw_image', + help = ''' + Crop images into sliding windows. + ''', + description = ''' + Crop images into sliding windows. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + sw_image_required = sw_image_parser.add_argument_group('required arguments') + + sw_image_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the sliding-window images should be written to. + ''', + required=True, + ) + sw_image_required.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Path to image directory. By default, will be inferred. + ''', + required = True, + ) + + # optional + sw_image_optional = sw_image_parser.add_argument_group('optional arguments') + sw_image_optional.add_argument( + '-s', '--window_size', + type = int, + nargs = '+', + help = ''' + Sliding window size in pixel. + If one argument is passed, quadratic windows of window_size will be generated. + If two arguments are passed, they are interpreted as window width and height, respectively. + + "-s 500", for example, crops sliding windows of size 500*500 (w*h), while "-s 500 300" crops + windows of size 500*300. + ''', + default=[1000], + ) + sw_image_optional.add_argument( + '-p', '--overlap', + type = int, + nargs = '+', + help = ''' + Overlap between sliding windows. + If one argument is passed, the same overlap is used in horizontal and vertical direction. + If two arguments are passed, they are interpreted as overlap in horizontal and + vertical, respectively. + ''', + default=[200], + ) + + # == sw_split + sw_split_parser = utils_parsers.add_parser( + 'sw_split', + help = ''' + Crop train-test-val-split (ginjinn split) images and corresponding annotations + into sliding windows. + ''', + description = ''' + Crop train-test-val-split (ginjinn split) images and corresponding annotations + into sliding windows. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + sw_split_required = sw_split_parser.add_argument_group('required arguments') + + sw_split_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Path to directory, which the sliding-window cropped data sets should be written to. + ''', + required=True, + ) + sw_split_required.add_argument( + '-i', '--split_dir', + type = str, + help = ''' + Path to directory generated by ginjinn split. + ''', + required=True, + ) + + # optional + sw_split_optional = sw_split_parser.add_argument_group('optional arguments') + sw_split_optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type. If 'auto', will be inferred. + ''', + choices = ['auto', 'COCO', 'PVOC'], + default = 'auto', + ) + sw_split_optional.add_argument( + '-s', '--window_size', + type = int, + nargs = '+', + help = ''' + Sliding window size in pixel. + If one argument is passed, quadratic windows of window_size will be generated. + If two arguments are passed, they are interpreted as window width and height, respectively. + + "-s 500", for example, crops sliding windows of size 500*500 (w*h), while "-s 500 300" crops + windows of size 500*300. + ''', + default=[1000], + ) + sw_split_optional.add_argument( + '-p', '--overlap', + type = int, + nargs = '+', + help = ''' + Overlap between sliding windows in pixel. + If one argument is passed, the same overlap is used in horizontal and vertical direction. + If two arguments are passed, they are interpreted as overlap in horizontal and + vertical, respectively. + ''', + default=[0.5], + ) + sw_split_optional.add_argument( + '-k', '--task', + choices = [ + 'instance-segmentation', 'bbox-detection' + ], + help = ''' + Task, which the dataset will be used for. Only applies to COCO + datasets. + ''', + default = 'instance-segmentation', + ) + sw_split_optional.add_argument( + '-m', '--img_id', + type = int, + help = ''' + Starting image ID for newly generated image annotations. + ''', + default=1, + ) + sw_split_optional.add_argument( + '-b', '--obj_id', + type = int, + help = ''' + Starting object ID for newly generated object annotations. + ''', + default=1, + ) + sw_split_optional.add_argument( + '-r', '--remove_empty', + dest = 'remove_empty', + action = 'store_true', + help = ''' + If this flag is set, cropped images without object annotation will + not be saved. + ''' + ) + parser.set_defaults(remove_empty = False) + sw_split_optional.add_argument( + '-c', '--remove_incomplete', + dest = 'remove_incomplete', + action = 'store_true', + help = ''' + If this flag is set, object annotations that are touched (trimmed) + by a sliding-window edge are removed from the corresponding sliding-window images. + ''' + ) + parser.set_defaults(remove_incomplete = False) + + # == sw_merge + sw_merge_parser = utils_parsers.add_parser( + 'sw_merge', + help = ''' + Merge sliding-window cropped images and annotations. + Objects will be merged only if they satisfy ALL three thresholds. + ''', + description = ''' + Merge sliding-window cropped images and annotations. + Objects will be merged only if they satisfy ALL three thresholds. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + sw_merge_required = sw_merge_parser.add_argument_group('required arguments') + sw_merge_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Output directory. Will be created if it does not exist. + ''', + required=True, + ) + sw_merge_required.add_argument( + '-i', '--image_dir', + type = str, + help = ''' + Path to directory containing the sliding-window cropped images. + ''', + required=True, + ) + sw_merge_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to the JSON annotation file. + ''', + required=True, + ) + sw_merge_required.add_argument( + '-t', '--task', + type = str, + choices = [ + 'instance-segmentation', 'bbox-detection' + ], + help = ''' + Task, which the dataset will be used for. + ''', + required = True, + ) + + # optional + sw_merge_optional = sw_merge_parser.add_argument_group('optional arguments') + sw_merge_optional.add_argument( + '-c', '--intersection_threshold', + type = int, + help = ''' + Absolute intersection threshold for merging in pixel. + ''', + default=0, + ) + sw_merge_optional.add_argument( + '-u', '--iou_threshold', + type = float, + help = ''' + Intersection over union threshold for merging in pixel. + ''', + default=0.5, + ) + sw_merge_optional.add_argument( + '-s', '--ios_threshold', + type = float, + help = ''' + Intersection over smaller object threshold for merging in pixel. + ''', + default=0.8, + ) + + + # == filter + filter_parser = utils_parsers.add_parser( + 'filter', + help = ''' + Filter annotation categories. + ''', + description = ''' + Filter annotation categories. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + filter_parser_required = filter_parser.add_argument_group('required arguments') + filter_parser_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Output directory the filtered annotations, and optionally filtered images + (see img_dir option), should be written to. + ''', + required=True, + ) + filter_parser_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to COCO annotation file (JSON) or PVCO annotation directory. + ''', + required=True, + ) + filter_parser_required.add_argument( + '-f', '--filter', + type = str, + help = ''' + Names of categories to filter. Filtering depends on the drop parameter. + By default, the passed categories are kept and the remaining ones are dropped. + ''', + action = 'append', + required = True, + ) + + # optional + filter_parser_optional = filter_parser.add_argument_group('optional arguments') + filter_parser_optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type. If "auto", will be inferred. + ''', + choices = ['auto', 'COCO', 'PVOC'], + default = 'auto', + ) + filter_parser_optional.add_argument( + '-d', '--drop', + action = 'store_true', + help = ''' + Drop categories in filter instead of keeping them. + ''' + ) + parser.set_defaults(drop = False) + + filter_parser_optional.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Directory containing the annotated images. Use this parameter if you + want to filter out images without annotation after category filtering. + ''', + required=False, + default=None, + ) + filter_parser_optional.add_argument( + '-c', '--copy_images', + action = 'store_true', + help = ''' + Copy images to img_dir instead of hard linkin them. + ''' + ) + parser.set_defaults(copy_images = False) + + # == filter_size + filter_size_parser = utils_parsers.add_parser( + 'filter_size', + help = ''' + Filter COCO object annotations by size. + ''', + description = ''' + Filter COCO object annotations by size. + ''', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + # required + filter_size_parser_required = filter_size_parser.add_argument_group('required arguments') + filter_size_parser_required.add_argument( + '-o', '--out_file', + type = str, + help = ''' + Annotation file (JSON) the filtered annotations should be written to. + ''', + required=True, + ) + filter_size_parser_required.add_argument( + '-a', '--ann_file', + type = str, + help = ''' + Path to COCO annotation file (JSON). + ''', + required=True, + ) + filter_size_parser_required.add_argument( + '-d', '--task', + type = str, + choices = [ + 'instance-segmentation', 'bbox-detection' + ], + help = ''' + Task, which the dataset will be used for. + ''', + required = True, + ) + + # optional + filter_size_parser_optional = filter_size_parser.add_argument_group( + 'optional arguments' + ) + filter_size_parser_optional.add_argument( + '-x', '--min_width', + type = int, + default = 5, + help = ''' + Minimal total object width. + ''' + ) + filter_size_parser_optional.add_argument( + '-y', '--min_height', + type = int, + default = 5, + help = ''' + Minimal total object height. + ''' + ) + filter_size_parser_optional.add_argument( + '-r', '--min_area', + type = int, + default = 25, + help = ''' + Minimal total object area. + ''' + ) + filter_size_parser_optional.add_argument( + '-f', '--min_fragment_area', + type = int, + default = 25, + help = ''' + Minimal object fragment area. + ''' + ) + + # == visualize + visualize_parser = utils_parsers.add_parser( + 'visualize', + help = ''' + Visualize object annotations on images. + ''', + description = ''' + Visualize object annotations on images. + ''', + aliases=['vis'], + ) + + # required + visualize_parser_required = visualize_parser.add_argument_group('required arguments') + visualize_parser_required.add_argument( + '-o', '--out_dir', + type = str, + help = ''' + Directory the visualizations should be written to. + ''', + required=True, + ) + visualize_parser_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to COCO annotation file (JSON) or PVCO annotation directory. + ''', + required=True, + ) + visualize_parser_required.add_argument( + '-v', '--vis_type', + type = str, + help = ''' + Visualization type. Either "bbox" for bounding-boxes or "segmentation" + for segmentation masks. For PVOC, only "bbox" is allowed. + ''', + choices = ['segmentation', 'bbox'], + required=True, + ) + + visualize_parser_optional = visualize_parser.add_argument_group('optional arguments') + visualize_parser_optional.add_argument( + '-i', '--img_dir', + type = str, + help = ''' + Directory containing (potentially a subset) of the annotated images. + By default, will be inferred. + ''', + default = None, + ) + visualize_parser_optional.add_argument( + '-t', '--ann_type', + type = str, + help = ''' + Annotation type. If "auto", will be inferred. + ''', + choices = ['auto', 'COCO', 'PVOC'], + default = 'auto' + ) + + # == count + count_parser = utils_parsers.add_parser( + 'count', + help = ''' + Count objects per category for each image annotation. + ''', + description = ''' + Count objects per category for each image annotation. + ''', + ) + + # required + count_parser_required = count_parser.add_argument_group('required arguments') + count_parser_required.add_argument( + '-a', '--ann_path', + type = str, + help = ''' + Path to COCO annotation file (JSON). + ''', + required=True, + ) + count_parser_required.add_argument( + '-o', '--out_file', + type = str, + help = ''' + File (CSV) the category counts should be written to. + ''', + required=True, + ) + + # == other utils + # ... + + return parser + +# Note: It is a deliberate decision not to subclass argparse.ArgumentParser. +# It might be preferable to work with composition instead of inheritance, +# since it might be desirable to include postprocessing steps after argparse +# parsing. +class GinjinnArgumentParser(): + '''GinjinnArgumentParser + + Class for setting up and handling commandline arguments. + ''' + + _description = ''' + GinJinn is a framework for simplifying the setup, training, evaluation, + and deployment of object detection models. + ''' + + def __init__(self): + self.parser = argparse.ArgumentParser( + description=self._description, + ) + self.parser.add_argument( + '-d', '--debug', + help='Debug mode', + action='store_true', + ) + + self._subparsers = self.parser.add_subparsers( + dest='subcommand', + help='GinJinn subcommands.' + ) + self._init_subparsers() + + def parse_args(self, args=None, namespace=None): + '''parse_args + Parses the commandline arguments and returns them in argparse + format. + + Parameters + ---------- + args + List of strings to parse. If None, the strings are taken from sys.argv. + namespace + An object to take the attributes. The default is a new empty argparse Namespace object. + + Returns + ------- + args + Parsed argparse arguments + ''' + + return self.parser.parse_args(args=args, namespace=namespace) + + def _init_subparsers(self): + '''_init_subparsers + + Initilialize parsers for GinJinn subcommands. + ''' + + _setup_new_parser(self._subparsers) + _setup_train_parser(self._subparsers) + _setup_evaluate_parser(self._subparsers) + _setup_predict_parser(self._subparsers) + _setup_split_parser(self._subparsers) + _setup_simulate_parser(self._subparsers) + _setup_info_parser(self._subparsers) + _setup_utils_parser(self._subparsers) + + # TODO: implement diff --git a/ginjinn/commandline/commandline_app.py b/ginjinn/commandline/commandline_app.py new file mode 100644 index 0000000..20c071d --- /dev/null +++ b/ginjinn/commandline/commandline_app.py @@ -0,0 +1,100 @@ +''' GinJinn commandline application code. +''' + +from .argument_parser import GinjinnArgumentParser + +class GinjinnCommandlineApplication(): + '''GinjinnCommandlineApplication + + GinJinn commandline application. + ''' + def __init__(self): + self.parser = GinjinnArgumentParser() + self.args = None + + def run(self, args=None, namespace=None): + '''run + Start GinJinn commandline application. + + Parameters + ---------- + args + List of strings to parse. If None, the strings are taken from sys.argv. + namespace + An object to take the attributes. The default is a new empty argparse Namespace object. + ''' + self.args = self.parser.parse_args(args=args, namespace=namespace) + # print(self.args) + + if self.args.subcommand == 'new': + self._run_new() + elif self.args.subcommand == 'split': + self._run_split() + elif self.args.subcommand == 'simulate': + self._run_simulate() + elif self.args.subcommand == 'train': + self._run_train() + elif self.args.subcommand == 'utils': + self._run_utils() + elif self.args.subcommand in ['evaluate', 'eval']: + self._run_evaluate() + elif self.args.subcommand == 'predict': + self._run_predict() + elif self.args.subcommand == 'info': + self._run_info() + + def _run_split(self): + '''_run_split + Run the GinJinn split command. + ''' + from .splitter import ginjinn_split + ginjinn_split(self.args) + + def _run_simulate(self): + '''_run_simulate + Run the GinJinn simulate command. + ''' + from .simulate import ginjinn_simulate + ginjinn_simulate(self.args) + + def _run_new(self): + '''_run_new + Run the GinJinn new command. + ''' + from .new import ginjinn_new + ginjinn_new(self.args) + + def _run_train(self): + '''_run_train + Run the GinJinn train command. + ''' + from .train import ginjinn_train + ginjinn_train(self.args) + + def _run_utils(self): + '''_run_utils + Run the GinJinn utils command. + ''' + from .utils import ginjinn_utils + ginjinn_utils(self.args) + + def _run_evaluate(self): + '''_run_evaluate + Run the GinJinn evaluate command. + ''' + from .evaluate import ginjinn_evaluate + ginjinn_evaluate(self.args) + + def _run_predict(self): + '''_run_predict + Run the GinJinn predict command. + ''' + from .predict import ginjinn_predict + ginjinn_predict(self.args) + + def _run_info(self): + '''_run_info + Run the GinJinn info command. + ''' + from .info import ginjinn_info + ginjinn_info(self.args) diff --git a/ginjinn/commandline/evaluate.py b/ginjinn/commandline/evaluate.py new file mode 100644 index 0000000..2c24507 --- /dev/null +++ b/ginjinn/commandline/evaluate.py @@ -0,0 +1,104 @@ +''' Module for the ginjinn evaluate subcommand. +''' + +import os +import sys +import pandas as pd +from ginjinn.ginjinn_config import GinjinnConfiguration +import ginjinn.ginjinn_config.config_error as config_error + +def write_evaluation( + eval_res: dict, + file_path: str, + sep=',', +): + '''write_evaluation + + Write evaluation results. + + Parameters + ---------- + eval_res : dict + Dictionary containing the evalaution results + file_path : str + Path the evaluation results should be written to. + ''' + + res_df = pd.DataFrame.from_dict(eval_res) + res_df.to_csv(file_path, sep=sep) + + + +def ginjinn_evaluate(args): + '''ginjinn_evaluate + + GinJinn evaluate command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn evaluate + subcommand. + ''' + + project_dir = args.project_dir + config_file = os.path.join(project_dir, 'ginjinn_config.yaml') + + if args.debug: + config = GinjinnConfiguration.from_config_file(config_file) + else: + try: + config = GinjinnConfiguration.from_config_file(config_file) + except config_error.InvalidInputConfigurationError as err: + print('\nInvalid input configuration:') + print(err) + sys.exit(1) + except config_error.InvalidModelConfigurationError as err: + print('\nInvalid model configuration:') + print(err) + sys.exit(1) + except config_error.InvalidAugmentationConfigurationError as err: + print('\nInvalid augmentation configuration:') + print(err) + sys.exit(1) + except config_error.InvalidGinjinnConfigurationError as err: + print('\nInvalid GinJinn configuration:') + print(err) + sys.exit(1) + except config_error.InvalidOptionsConfigurationError as err: + print('\nInvalid options configuration:') + print(err) + sys.exit(1) + except config_error.InvalidTrainingConfigurationError as err: + print('\nInvalid training configuration:') + print(err) + sys.exit(1) + except Exception as any_e: + raise any_e + + # import here to reduce startup time when train is not called. + from ginjinn.evaluation import evaluate + from ginjinn.data_reader.load_datasets import load_test_set + + # checkpoint + checkpoint_name = args.checkpoint + checkpoint_file = os.path.join( + config.project_dir, 'outputs', checkpoint_name + ) + if not os.path.isfile(checkpoint_file): + print( + f'\nERROR: Checkpoint "{checkpoint_name}" (expected location: {checkpoint_file}) ' +\ + 'does not exist. Please pass a valid checkpoint name.' + ) + sys.exit(1) + + # register data set globally + load_test_set(config) + + # evaluate + res = evaluate(config, checkpoint_name=checkpoint_name) + + # write evaluation results + eval_res_file = os.path.join(config.project_dir, 'evaluation.csv') + write_evaluation(res, eval_res_file) + print(f'Evaluation results written to "{eval_res_file}".') diff --git a/ginjinn/commandline/info.py b/ginjinn/commandline/info.py new file mode 100644 index 0000000..06ac2a0 --- /dev/null +++ b/ginjinn/commandline/info.py @@ -0,0 +1,41 @@ +''' Module for the ginjinn info subcommand +''' + +import sys + +def ginjinn_info(args): + '''ginjinn_info + + GinJinn info command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn + info subcommand. + ''' + from ginjinn.utils.utils import dataset_info, find_img_dir, get_anntype, ImageDirNotFound + + ann_path = args.ann_path + img_dir = args.img_dir + ann_type = args.ann_type + + if not img_dir: + try: + img_dir = find_img_dir(ann_path) + except ImageDirNotFound: + print( + f'ERROR: could not find "images" folder as sibling of "{ann_path}". Make sure ' +\ + f'there is an "images" folder in the same directory as "{ann_path}" or ' +\ + 'explicitly pass "--image_dir".' + ) + sys.exit() + + if ann_type == 'auto': + ann_type = get_anntype(ann_path) + + dataset_info( + ann_path=ann_path, + img_dir=img_dir, + ann_type=ann_type, + ) diff --git a/ginjinn/commandline/main.py b/ginjinn/commandline/main.py new file mode 100644 index 0000000..da16a05 --- /dev/null +++ b/ginjinn/commandline/main.py @@ -0,0 +1,13 @@ +''' Commandline main +''' + +from .commandline_app import GinjinnCommandlineApplication + +def main(): + '''main + GinJinn main. + ''' + app = GinjinnCommandlineApplication() + app.run() + # print(app.args) + # print('GinJinn called!') diff --git a/ginjinn/commandline/new.py b/ginjinn/commandline/new.py new file mode 100644 index 0000000..bdbb9c4 --- /dev/null +++ b/ginjinn/commandline/new.py @@ -0,0 +1,239 @@ +''' Module for the ginjinn new subcommand +''' + +import shutil +import sys +import os +import re + +import pkg_resources + +from ginjinn.utils import confirmation_cancel +from ginjinn.utils.utils import get_dstype + +TYPE_RE = re.compile(r'(type:\s+")\w+(")') +TRAINING_RE = re.compile( + r'^(\s*training:\s*)(annotation_path:\s+").+("\s*)(image_path:\s*").+(")\s*$', + re.MULTILINE +) +VALIDATION_RE = re.compile( + r'^(\s*validation:\s*)(annotation_path:\s+").+("\s*)(image_path:\s*").+(")\s*$', + re.MULTILINE +) +TEST_RE = re.compile( + r'^(\s*test:\s*)(annotation_path:\s+").+("\s*)(image_path:\s*").+(")\s*$', + re.MULTILINE +) + +RE_MAP = { + 'train': TRAINING_RE, + 'val': VALIDATION_RE, + 'test': TEST_RE, +} + +def ginjinn_new(args): + '''ginjinn_new + + GinJinn new command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn new + subcommand. + ''' + + template_dir = pkg_resources.resource_filename( + 'ginjinn', 'data/ginjinn_config/templates', + ) + template_path = os.path.join( + template_dir, + ('adv_' if args.advanced else '') + args.template + ) + + + project_dir = args.project_dir + if os.path.exists(project_dir): + if confirmation_cancel( + f'\nDirectory "{project_dir}" already exists.\nDo you want to overwrite it? ' + \ + f'WARNING: this will delete "{project_dir}" and ALL SUBDIRECTORIES!\n' + ): + shutil.rmtree(project_dir) + else: + sys.exit() + + os.mkdir(project_dir) + os.mkdir(os.path.join(project_dir, 'outputs')) + + with open(template_path) as cfg_template_file: + config_str = cfg_template_file.read() + config_str = config_str.replace('"ENTER PROJECT DIR HERE"', f'{os.path.abspath(project_dir)}') + + # update config if data_dir is passed + data_dir = args.data_dir + if not data_dir is None: + print(f'Searching for dataset in "{data_dir}" ...') + data_dir_contents = os.listdir(data_dir) + + if 'train' in data_dir_contents: + ds_types = [] + print('... found "train"') + print('... expecting a ginjinn split dataset') + for ds_name in ['train', 'val', 'test']: + if not ds_name in data_dir_contents: + print(f'... could not find "{ds_name}"') + config_str = re.sub( + RE_MAP[ds_name], + r'#\1#\2\3#\4\5', + config_str + ) + print(f'\t... commented out {ds_name} dataset.') + continue + + print(f'... found "{ds_name}"') + ds_path = os.path.join(data_dir, ds_name) + if not os.path.isdir(ds_path): + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + f'("{ds_name}" is not a directory)' + ) + sys.exit() + + images_path = os.path.join(ds_path, 'images') + if not os.path.isdir(images_path): + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + f'("{ds_name}/images" is not a directory)' + ) + sys.exit() + + ds_dir_contents = os.listdir(ds_path) + # COCO + if 'annotations.json' in ds_dir_contents: + print('\t... found "annotations.json"') + print('\t... expecting a COCO dataset') + annotations_path = os.path.join(ds_path, 'annotations.json') + if not os.path.isfile(annotations_path): + print( + f'ERROR: "{data_dir}/{ds_name}" is not a valid data directory. ' +\ + '(expecting COCO dataset but "annotations.json" is not a file)' + ) + sys.exit() + + ann_path_rel = os.path.relpath(annotations_path, project_dir) + img_path_rel = os.path.relpath(images_path, project_dir) + config_str = re.sub( + RE_MAP[ds_name], + f'\\1\\2{ann_path_rel}\\3\\4{img_path_rel}\\5', + config_str + ) + print(f'\tFound COCO dataset and initialized it as {ds_name} data.') + ds_types.append('COCO') + + # PVOC + elif 'annotations' in ds_dir_contents: + print('\t... found "annotations"') + print('\t... expecting a PVOC dataset') + + annotations_path = os.path.join(ds_path, 'annotations') + if not os.path.isdir(annotations_path): + print( + f'ERROR: "{data_dir}/{ds_name}" is not a valid data directory. ' +\ + '(expecting PVOC dataset but "annotations" is not a directory)' + ) + sys.exit() + + ann_path_rel = os.path.relpath(annotations_path, project_dir) + img_path_rel = os.path.relpath(images_path, project_dir) + config_str = re.sub( + RE_MAP[ds_name], + f'\\1\\2{ann_path_rel}\\3\\4{img_path_rel}\\5', + config_str + ) + print(f'\tFound PVOC dataset and initialized it as {ds_name} data.') + ds_types.append('PVOC') + + if len(set(ds_types)) > 1: + print('ERROR: incompatible dataset types found.') + sys.exit() + config_str = re.sub(TYPE_RE, f'\\1{ds_types[0]}\\2', config_str) + + elif 'images' in data_dir_contents: + print('... found "images"') + images_path = os.path.join(data_dir, 'images') + if not os.path.isdir(images_path): + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + '("images" is not a directory)' + ) + sys.exit() + # COCO + if 'annotations.json' in data_dir_contents: + print('... found "annotations.json"') + print('... expecting a COCO dataset') + annotations_path = os.path.join(data_dir, 'annotations.json') + if not os.path.isfile(annotations_path): + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + '(expecting COCO dataset but "annotations.json" is not a file)' + ) + sys.exit() + + + ann_path_rel = os.path.relpath(annotations_path, project_dir) + img_path_rel = os.path.relpath(images_path, project_dir) + config_str = re.sub(TYPE_RE, r'\1COCO\2', config_str) + config_str = re.sub( + TRAINING_RE, + f'\\1\\2{ann_path_rel}\\3\\4{img_path_rel}\\5', + config_str + ) + print('Found COCO dataset and initialized it as training data.') + + # PVOC + elif 'annotations' in data_dir_contents: + print('... found "annotations"') + print('... expecting a PVOC dataset') + + annotations_path = os.path.join(data_dir, 'annotations') + if not os.path.isdir(annotations_path): + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + '(expecting COCO dataset but "annotations" is not a directory)' + ) + sys.exit() + + ann_path_rel = os.path.relpath(annotations_path, project_dir) + img_path_rel = os.path.relpath(images_path, project_dir) + config_str = re.sub(TYPE_RE, r'\1PVOC\2', config_str) + config_str = re.sub( + TRAINING_RE, + f'\\1\\2{ann_path_rel}\\3\\4{img_path_rel}\\5', + config_str + ) + print('Found PVOC dataset and initialized it as training data.') + + config_str = re.sub( + VALIDATION_RE, + r'#\1#\2\3#\4\5', + config_str + ) + config_str = re.sub( + TEST_RE, + r'#\1#\2\3#\4\5', + config_str + ) + print('Commented out validation and test datasets.') + + else: + print( + f'ERROR: "{data_dir}" is not a valid data directory. ' +\ + '(no valid dataset found)' + ) + sys.exit() + + config_path = os.path.join(project_dir, 'ginjinn_config.yaml') + with open(config_path, 'w') as cfg_file: + cfg_file.write(config_str) + + print(f'Initialized GinJinn project at "{project_dir}".') diff --git a/ginjinn/commandline/predict.py b/ginjinn/commandline/predict.py new file mode 100644 index 0000000..ab51738 --- /dev/null +++ b/ginjinn/commandline/predict.py @@ -0,0 +1,115 @@ +''' Module for the ginjinn predict subcommand. +''' + +import os +import sys +from ginjinn.ginjinn_config import GinjinnConfiguration +import ginjinn.ginjinn_config.config_error as config_error + +def ginjinn_predict(args): + '''ginjinn_predict + + GinJinn predict command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn predict + subcommand. + ''' + + project_dir = args.project_dir + config_file = os.path.join(project_dir, 'ginjinn_config.yaml') + + if args.debug: + config = GinjinnConfiguration.from_config_file(config_file) + else: + try: + config = GinjinnConfiguration.from_config_file(config_file) + except config_error.InvalidInputConfigurationError as err: + print('\nInvalid input configuration:') + print(err) + sys.exit(1) + except config_error.InvalidModelConfigurationError as err: + print('\nInvalid model configuration:') + print(err) + sys.exit(1) + except config_error.InvalidAugmentationConfigurationError as err: + print('\nInvalid augmentation configuration:') + print(err) + sys.exit(1) + except config_error.InvalidGinjinnConfigurationError as err: + print('\nInvalid GinJinn configuration:') + print(err) + sys.exit(1) + except config_error.InvalidOptionsConfigurationError as err: + print('\nInvalid options configuration:') + print(err) + sys.exit(1) + except config_error.InvalidTrainingConfigurationError as err: + print('\nInvalid training configuration:') + print(err) + sys.exit(1) + except Exception as any_e: + raise any_e + + image_path = args.image_path + + # input + img_dir = None + img_names = [] + if os.path.isdir(image_path): + img_dir = image_path + else: + img_names = [image_path] + + # checkpoint + checkpoint_name = args.checkpoint + checkpoint_file = os.path.join( + config.project_dir, 'outputs', checkpoint_name + ) + if not os.path.isfile(checkpoint_file): + print( + f'\nERROR: Checkpoint "{checkpoint_name}" (expected location: {checkpoint_file}) ' +\ + 'does not exist. Please pass a valid checkpoint name.' + ) + sys.exit(1) + + # output + out_dir = args.out_dir + if out_dir is None: + out_dir = os.path.join(config.project_dir, 'prediction') + if not os.path.exists(out_dir): + os.mkdir(out_dir) + else: + if not os.path.exists(out_dir): + os.mkdir(out_dir) + + # other + threshold = args.threshold + padding = args.padding + seg_refinement = args.seg_refinement + refinement_method = args.refinement_method + device = args.device + + output_options = args.output_types + output_options = list({x if not isinstance(x, list) else x[0] for x in output_options}) + + from ginjinn.predictor import GinjinnPredictor + predictor = GinjinnPredictor.from_ginjinn_config( + gj_cfg=config, + img_dir=img_dir, + outdir=out_dir, + checkpoint_name=checkpoint_name, + ) + predictor.predict( + img_names=img_names, + output_options=output_options, + padding=padding, + seg_refinement=seg_refinement, + refinement_device=device, + refinement_method=refinement_method, + threshold=threshold, + ) + + print(f'Predictions written to {out_dir}.') diff --git a/ginjinn/commandline/simulate.py b/ginjinn/commandline/simulate.py new file mode 100644 index 0000000..5df40f5 --- /dev/null +++ b/ginjinn/commandline/simulate.py @@ -0,0 +1,124 @@ +''' Module for the simulate subcommand. +''' + +import os +import sys +import shutil + +from ginjinn.utils import confirmation_cancel +from ginjinn.simulation import generate_simple_shapes_coco, generate_simple_shapes_pvoc + + +def ginjinn_simulate(args): + '''ginjinn_simulate + + GinJinn simulate command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn simulate + subcommand. + + Raises + ------ + Exception + Raised if unknown subcommand passed. + ''' + if args.simulate_subcommand == 'shapes': + simulate_shapes(args) + else: + err = f'Unknown simulate subcommand "{args.simulate_subcommand}".' + raise Exception(err) + +def simulate_shapes(args): + '''simulate_shapes + + GinJinn simulate shapes command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn simulate shapes + subcommand. + ''' + + out_dir = args.out_dir + + if os.path.exists(out_dir): + if confirmation_cancel( + f'\nDirectory "{out_dir}" already exists.\nDo you want to overwrite it? ' + \ + f'WARNING: this will delete "{out_dir}" and ALL SUBDIRECTORIES!\n' + ): + shutil.rmtree(out_dir) + else: + sys.exit() + + if args.ann_type == 'COCO': + img_dir = os.path.join(out_dir, 'images') + ann_path = os.path.join(out_dir, 'annotations.json') + + os.mkdir(out_dir) + os.mkdir(img_dir) + + circle_col = args.circle_col.lstrip('#') + triangle_col = args.triangle_col.lstrip('#') + circle_col = [int(circle_col[i:i+2], 16) / 255 for i in (0, 2, 4)] + triangle_col = [int(triangle_col[i:i+2], 16) / 255 for i in (0, 2, 4)] + + generate_simple_shapes_coco( + img_dir=img_dir, + ann_file=ann_path, + n_images=args.n_images, + min_w=args.min_w, + max_w=args.max_w, + min_h=args.min_h, + max_h=args.max_h, + min_n_shapes=args.min_n_shapes, + max_n_shapes=args.max_n_shapes, + circle_col=circle_col, + triangle_col=triangle_col, + col_var=args.color_variance, + min_r=args.min_shape_radius, + max_r=args.max_shape_radius, + min_rot=args.min_shape_angle, + max_rot=args.max_shape_angle, + noise=args.noise, + ) + + print(f'Simulated dataset written to "{out_dir}".') + + if args.ann_type == 'PVOC': + img_dir = os.path.join(out_dir, 'images') + ann_dir = os.path.join(out_dir, 'annotations') + + os.mkdir(out_dir) + os.mkdir(img_dir) + os.mkdir(ann_dir) + + circle_col = args.circle_col.lstrip('#') + triangle_col = args.triangle_col.lstrip('#') + circle_col = [int(circle_col[i:i+2], 16) / 255 for i in (0, 2, 4)] + triangle_col = [int(triangle_col[i:i+2], 16) / 255 for i in (0, 2, 4)] + + generate_simple_shapes_pvoc( + img_dir=img_dir, + ann_dir=ann_dir, + n_images=args.n_images, + min_w=args.min_w, + max_w=args.max_w, + min_h=args.min_h, + max_h=args.max_h, + min_n_shapes=args.min_n_shapes, + max_n_shapes=args.max_n_shapes, + circle_col=circle_col, + triangle_col=triangle_col, + col_var=args.color_variance, + min_r=args.min_shape_radius, + max_r=args.max_shape_radius, + min_rot=args.min_shape_angle, + max_rot=args.max_shape_angle, + noise=args.noise, + ) + + print(f'Simulated dataset written to "{out_dir}".') diff --git a/ginjinn/commandline/splitter.py b/ginjinn/commandline/splitter.py new file mode 100644 index 0000000..d46fde6 --- /dev/null +++ b/ginjinn/commandline/splitter.py @@ -0,0 +1,128 @@ +''' GinJinn split commandline module +''' + +import pandas as pd +import sys + +from ginjinn.utils import confirmation_cancel + +def on_split_dir_exists(split_dir: str) -> bool: + '''on_split_dir_exists + + Callback for when the split output directory already exists. + + Parameters + ---------- + split_dir : str + Path to split directory. + + Returns + ------- + bool + Whether the existing directory should be overwritten. + ''' + return confirmation_cancel( + '"' + split_dir + '" already exists.\nDo you want do overwrite it? '+\ + 'ATTENTION: This will DELETE "' + split_dir + '" and all subdirectories.\n' + ) + +def on_split_proposal(split_df: 'pd.DataFrame') -> bool: + '''on_split_proposal + + Callback for proposing a split. + + Parameters + ---------- + split_df : 'pd.DataFrame' + pandas.DataFrame containing split information. + + Returns + ------- + bool + Whether the proposed split should be accepted. + ''' + + df_pretty = pd.DataFrame( + [[f'{a} ({round(b, 2)})' for a,b in zip(r, r / r.sum())] for _, r in split_df.iterrows()], + columns=split_df.columns, + index=split_df.index + ) + + print('\nSplit proposal:') + print(df_pretty) + return confirmation_cancel( + '\nDo you want to accept this split? (Otherwise a new one will be generated.)\n' + ) + +def on_no_valid_split() -> bool: + '''on_no_valid_split + + Callback for when no valid split was found. + + + Returns + ------- + bool + Whether another try for finding a valid split should be made. + ''' + + return confirmation_cancel( + 'Could not find a valid split. Try again?\n' + ) + + +def ginjinn_split(args): + '''ginjinn_split + + GinJinn split command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn split + subcommand. + ''' + + # print('Running ginjinn split') + # print(args) + + # import here to avoid long startup time, when ginjinn_split is not called + from ginjinn.data_reader.data_splitter import create_split_2 + from ginjinn.utils.utils import get_anntype, find_img_dir, ImageDirNotFound + + ann_path = args.annotation_path + img_dir = args.image_dir + split_dir = args.output_dir + task = args.task + ann_type = args.ann_type + + if not img_dir: + try: + img_dir = find_img_dir(ann_path) + except ImageDirNotFound: + print( + f'ERROR: could not find "images" folder as sibling of "{ann_path}". Make sure ' +\ + f'there is an "images" folder in the same directory as "{ann_path}" or ' +\ + 'explicitly pass "--image_dir".' + ) + sys.exit() + + if ann_type == 'auto': + ann_type = get_anntype(ann_path) + + p_val = args.validation_fraction + p_test = args.test_fraction + + if create_split_2( + ann_path=ann_path, + img_path=img_dir, + split_dir=split_dir, + task=task, + ann_type=ann_type, + p_val=p_val, + p_test=p_test, + on_split_dir_exists=on_split_dir_exists, + on_split_proposal=on_split_proposal, + on_no_valid_split=on_no_valid_split, + ): + print(f'Datasets written to "{split_dir}".') diff --git a/ginjinn/commandline/tests/test_argument_parser.py b/ginjinn/commandline/tests/test_argument_parser.py new file mode 100644 index 0000000..708fab2 --- /dev/null +++ b/ginjinn/commandline/tests/test_argument_parser.py @@ -0,0 +1,30 @@ +import pytest +import subprocess + +from ginjinn.commandline import GinjinnArgumentParser + +def test_simple_(): + # new + p = GinjinnArgumentParser() + args = p.parse_args(['new', 'my_project_dir']) + + # train + args = p.parse_args(['train', 'my_project_dir']) + + # evaluate + args = p.parse_args(['evaluate', 'my_project_dir']) + + # predict + args = p.parse_args(['predict', 'my_project_dir', '-i', 'test']) + + # split + args = p.parse_args([ + 'split', + '-i', 'my_image_dir', + '-a', 'my_annotations.json', + '-o', 'my_split_dir', + '-d', 'instance-segmentation', + '-k', 'COCO', + '-t', '0.2', + '-v', '0.2' + ]) diff --git a/ginjinn/commandline/tests/test_cmd_main.py b/ginjinn/commandline/tests/test_cmd_main.py new file mode 100644 index 0000000..d4ab360 --- /dev/null +++ b/ginjinn/commandline/tests/test_cmd_main.py @@ -0,0 +1,305 @@ +# import pytest +# import sys +# import copy +# import tempfile +# import os +# import mock +# import pkg_resources +# import yaml + +# from ginjinn.commandline import main, commandline_app, argument_parser +# from ginjinn.commandline import splitter +# from ginjinn.commandline import simulate +# from ginjinn.commandline import train + +# from ginjinn.simulation import generate_simple_shapes_coco, generate_simple_shapes_pvoc + +# from detectron2.data import DatasetCatalog + +# @pytest.fixture(scope='module', autouse=True) +# def tmp_dir(): +# tmpdir = tempfile.TemporaryDirectory() + +# yield tmpdir.name + +# tmpdir.cleanup() + +# @pytest.fixture(scope='module') +# def simulate_coco(tmp_dir): +# sim_dir = os.path.join(tmp_dir, 'sim_coco') +# os.mkdir(sim_dir) + +# img_dir = os.path.join(sim_dir, 'images') +# os.mkdir(img_dir) +# ann_path = os.path.join(sim_dir, 'annotations.json') +# generate_simple_shapes_coco( +# img_dir=img_dir, ann_file=ann_path, n_images=40, +# ) +# return img_dir, ann_path + +# @pytest.fixture(scope='module') +# def simulate_pvoc(tmp_dir): +# sim_dir = os.path.join(tmp_dir, 'sim_pvoc') +# os.mkdir(sim_dir) + +# img_dir = os.path.join(sim_dir, 'images') +# os.mkdir(img_dir) +# ann_dir = os.path.join(sim_dir, 'annotations') +# os.mkdir(ann_dir) + +# generate_simple_shapes_pvoc( +# img_dir=img_dir, ann_dir=ann_dir, n_images=40, +# ) +# return img_dir, ann_dir + +# @pytest.fixture(scope='module', autouse=True) +# def example_config(tmp_dir, simulate_coco): +# img_dir, ann_path = simulate_coco + +# example_config_1_path = pkg_resources.resource_filename( +# 'ginjinn', 'data/ginjinn_config/example_config_1.yaml', +# ) + +# with open(example_config_1_path) as config_f: +# config = yaml.load(config_f) + +# config['input']['training']['annotation_path'] = ann_path +# config['input']['training']['image_path'] = img_dir + +# config_dir = os.path.join(tmp_dir, 'example_config') +# os.mkdir(config_dir) +# config['project_dir'] = os.path.abspath(config_dir) + +# config_file = os.path.join(config_dir, 'ginjinn_config.yaml') +# with open(config_file, 'w') as config_f: +# yaml.dump(config, config_f) + +# return (config, config_file) + +# @pytest.fixture(scope='module', autouse=True) +# def example_project(tmp_dir, example_config): +# config, _ = example_config + +# project_dir = os.path.join(tmp_dir, 'example_project') +# config['project_dir'] = project_dir +# os.mkdir(project_dir) +# os.mkdir(os.path.join(project_dir, 'outputs')) + +# config_file = os.path.join(project_dir, 'ginjinn_config.yaml') +# with open(config_file, 'w') as config_f: +# yaml.dump(config, config_f) + +# return project_dir + +# @pytest.fixture(scope='module', autouse=True) +# def example_config_pvoc(tmp_dir, simulate_pvoc): +# img_dir, ann_dir = simulate_pvoc + +# example_config_1_path = pkg_resources.resource_filename( +# 'ginjinn', 'data/ginjinn_config/example_config_1.yaml', +# ) + +# with open(example_config_1_path) as config_f: +# config = yaml.load(config_f) + +# config['task'] = 'bbox-detection' + +# config['model']['name'] = 'faster_rcnn_R_50_FPN_3x' + +# config['input']['type'] = 'PVOC' +# config['input']['training']['annotation_path'] = ann_dir +# config['input']['training']['image_path'] = img_dir + +# config_dir = os.path.join(tmp_dir, 'example_config_pvoc') +# os.mkdir(config_dir) +# config['project_dir'] = os.path.abspath(config_dir) + +# config_file = os.path.join(config_dir, 'ginjinn_config.yaml') +# with open(config_file, 'w') as config_f: +# yaml.dump(config, config_f) + +# return (config, config_file) + +# @pytest.fixture(scope='module', autouse=True) +# def example_project_pvoc(tmp_dir, example_config_pvoc): +# config, _ = example_config_pvoc + +# project_dir = os.path.join(tmp_dir, 'example_project_pvoc') +# config['project_dir'] = project_dir +# os.mkdir(project_dir) +# os.mkdir(os.path.join(project_dir, 'outputs')) + +# config_file = os.path.join(project_dir, 'ginjinn_config.yaml') +# with open(config_file, 'w') as config_f: +# yaml.dump(config, config_f) + +# return project_dir + +# def test_main_simple(tmp_dir): +# project_dir = os.path.join(tmp_dir, 'test_new_0') + +# tmp = copy.deepcopy(sys.argv) +# sys.argv = ['ginjinn', 'new', project_dir] +# main() +# sys.argv = tmp + +# def test_splitting(tmp_dir, simulate_coco): +# img_dir, ann_path = simulate_coco + + +# split_dir = os.path.join(tmp_dir, 'test_splitting_0') +# os.mkdir(split_dir) + +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'split', +# '-i', img_dir, +# '-a', ann_path, +# '-o', split_dir, +# '-d', 'instance-segmentation', +# '-k', 'COCO' +# ] +# ) + +# def y_gen(): +# while True: +# yield 'y' +# y_it = y_gen() + +# def y(*args, **kwargs): +# return next(y_it) + +# with mock.patch('builtins.input', y): +# splitter.ginjinn_split(args) + +# with mock.patch('builtins.input', y): +# splitter.ginjinn_split(args) + +# with mock.patch('builtins.input', lambda *args: 'n'): +# splitter.ginjinn_split(args) + +# def test_splitting_pvoc(tmp_dir, simulate_pvoc): +# img_dir, ann_dir = simulate_pvoc + + +# split_dir = os.path.join(tmp_dir, 'test_splitting_pvoc_0') +# os.mkdir(split_dir) + +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'split', +# '-i', img_dir, +# '-a', ann_dir, +# '-o', split_dir, +# '-d', 'bbox-detection', +# '-k', 'PVOC' +# ] +# ) + +# def y_gen(): +# while True: +# yield 'y' +# y_it = y_gen() + +# def y(*args, **kwargs): +# return next(y_it) + +# with mock.patch('builtins.input', y): +# splitter.ginjinn_split(args) + +# with mock.patch('builtins.input', y): +# splitter.ginjinn_split(args) + +# with mock.patch('builtins.input', lambda *args: 'n'): +# splitter.ginjinn_split(args) + +# def test_simulate(tmp_dir): +# simulate_dir = os.path.join(tmp_dir, 'test_simulate_0') + +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'simulate', +# 'shapes', +# '-o', simulate_dir, +# '-n', '5', +# ] +# ) + +# simulate.ginjinn_simulate(args) + +# with mock.patch('builtins.input', lambda *args: 'y'): +# simulate.ginjinn_simulate(args) + +# def test_simulate_pvoc(tmp_dir): +# simulate_dir = os.path.join(tmp_dir, 'test_simulate_pvoc_0') + +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'simulate', +# 'shapes', +# '-a', 'PVOC', +# '-o', simulate_dir, +# '-n', '5', +# ] +# ) + +# simulate.ginjinn_simulate(args) + +# with mock.patch('builtins.input', lambda *args: 'y'): +# simulate.ginjinn_simulate(args) + +# def test_train(example_project): +# project_dir = example_project +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'train', +# project_dir +# ] +# ) + +# try: +# DatasetCatalog.remove('train') +# except: +# pass +# try: +# DatasetCatalog.remove('val') +# except: +# pass + +# try: +# train.ginjinn_train(args) +# except AssertionError as err: +# if 'NVIDIA driver' in str(err): +# Warning(str(err)) +# else: +# raise err +# except Exception as err: +# raise err + +# def test_train_pvoc(example_project_pvoc): +# project_dir = example_project_pvoc +# args = argument_parser.GinjinnArgumentParser().parse_args( +# [ +# 'train', +# project_dir +# ] +# ) + +# try: +# DatasetCatalog.remove('train') +# except: +# pass +# try: +# DatasetCatalog.remove('val') +# except: +# pass + +# try: +# train.ginjinn_train(args) +# except AssertionError as err: +# if 'NVIDIA driver' in str(err): +# Warning(str(err)) +# else: +# raise err +# except Exception as err: +# raise err diff --git a/ginjinn/commandline/train.py b/ginjinn/commandline/train.py new file mode 100644 index 0000000..7d93f8c --- /dev/null +++ b/ginjinn/commandline/train.py @@ -0,0 +1,138 @@ +''' Module for the ginjinn train subcommand. +''' + +import os +import sys +import glob +from ginjinn.ginjinn_config import GinjinnConfiguration +from ginjinn.utils import confirmation_cancel +import ginjinn.ginjinn_config.config_error as config_error + +def write_training( + train_res: dict, + file_path: str, +): + '''write_training [summary] + + Parameters + ---------- + train_res : dict + [description] + file_path : str + [description] + ''' + +def ginjinn_train(args): + '''ginjinn_train + + GinJinn train command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn train + subcommand. + ''' + + # import here to reduce startup time when train is not called. + from ginjinn.data_reader.load_datasets import load_train_val_sets + from ginjinn.trainer import ValTrainer, Trainer + + project_dir = args.project_dir + config_file = os.path.join(project_dir, 'ginjinn_config.yaml') + + if args.debug: + config = GinjinnConfiguration.from_config_file(config_file) + else: + try: + config = GinjinnConfiguration.from_config_file(config_file) + except config_error.InvalidInputConfigurationError as err: + print('\nInvalid input configuration:') + print(err) + sys.exit(1) + except config_error.InvalidModelConfigurationError as err: + print('\nInvalid model configuration:') + print(err) + sys.exit(1) + except config_error.InvalidAugmentationConfigurationError as err: + print('\nInvalid augmentation configuration:') + print(err) + sys.exit(1) + except config_error.InvalidGinjinnConfigurationError as err: + print('\nInvalid GinJinn configuration:') + print(err) + sys.exit(1) + except config_error.InvalidOptionsConfigurationError as err: + print('\nInvalid options configuration:') + print(err) + sys.exit(1) + except config_error.InvalidTrainingConfigurationError as err: + print('\nInvalid training configuration:') + print(err) + sys.exit(1) + except Exception as any_e: + raise any_e + + resume = args.resume if not args.resume is None else config.options.resume + + # file cleanup + force = args.force + if not resume: + outputs_dir = os.path.join(config.project_dir, 'outputs') + metrics_file = os.path.join(outputs_dir, 'metrics.json') + + to_remove = [] + + if os.path.exists(metrics_file): + if force or confirmation_cancel( + 'WARNING: Resume is set to False but an old metrics.json file was found. ' +\ + 'Should the metrics.json file be removed? Keeping it will cause ' +\ + 'problems with plotting of the training metrics.\n' + ): + to_remove.append(metrics_file) + + events_files = glob.glob(os.path.join(outputs_dir, 'events.out.tfevents.*')) + if len(events_files) > 0: + if force or confirmation_cancel( + 'WARNING: Resume is set to False but old events files were found. ' +\ + 'Should the events files be removed? ' +\ + 'Keeping them will cause problems when inspecting ' +\ + 'the training with TensorBoard.\n' + ): + to_remove = to_remove + events_files + + other_output_files = [ + *glob.glob(os.path.join(outputs_dir, 'model_*')), + *glob.glob(os.path.join(outputs_dir, 'inference', '*')), + *glob.glob(os.path.join(outputs_dir, 'last_checkpoint')), + *glob.glob(os.path.join(outputs_dir, 'test_*')), + *glob.glob(os.path.join(outputs_dir, 'instances_predictions.pth')), + *glob.glob(os.path.join(outputs_dir, 'coco_instances_results.json')), + *glob.glob(os.path.join(outputs_dir, 'metrics.pdf')), + ] + if len(other_output_files) > 0: + if force or confirmation_cancel( + 'WARNING: Resume is set to False but old intermediate files were found. ' +\ + 'Should the intermediate files be removed?\n' + ): + to_remove = to_remove + other_output_files + + for f_path in to_remove: + os.remove(f_path) + + # register dataset(s) globally + load_train_val_sets(config) + + # overwrite max_iter if passed as commandline argument + if not args.n_iter is None: + config.training.max_iter = args.n_iter + + if config.input.val: + trainer = ValTrainer.from_ginjinn_config(config) + else: + trainer = Trainer.from_ginjinn_config(config) + + trainer.resume_or_load(resume=resume) + train_res = trainer.train() + + print(train_res) diff --git a/ginjinn/commandline/utils.py b/ginjinn/commandline/utils.py new file mode 100644 index 0000000..241e5cf --- /dev/null +++ b/ginjinn/commandline/utils.py @@ -0,0 +1,750 @@ +''' Module for the ginjinn utils subcommand. +''' + +import os +import shutil +import sys + +from ginjinn.utils import confirmation_cancel +from ginjinn.utils import flatten_coco + + +def ginjinn_utils(args): + '''ginjinn_utils + + GinJinn utils command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + subcommand. + + Raises + ------ + Exception + Raised if an unknown utils subcommand is passed. + ''' + + if args.utils_subcommand == 'merge': + utils_merge(args) + elif args.utils_subcommand == 'cleanup': + utils_cleanup(args) + elif args.utils_subcommand == 'flatten': + utils_flatten(args) + elif args.utils_subcommand == 'crop': + utils_crop(args) + elif args.utils_subcommand == 'sliding_window': + utils_sliding_window(args) + elif args.utils_subcommand == 'sw_image': + utils_sw_images(args) + elif args.utils_subcommand == 'sw_split': + utils_sw_split(args) + elif args.utils_subcommand == 'sw_merge': + utils_sw_merge(args) + elif args.utils_subcommand == 'filter': + utils_filter(args) + elif args.utils_subcommand == 'filter_size': + utils_filter_size(args) + elif args.utils_subcommand in ['visualize', 'vis']: + utils_visualize(args) + elif args.utils_subcommand == 'count': + utils_count(args) + else: + err = f'Unknown utils subcommand "{args.utils_subcommand}".' + raise Exception(err) + +def utils_merge(args): + '''utils_merge + + GinJinn utils merge command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils merge + subcommand. + ''' + + from ginjinn.data_reader.merge_datasets import merge_datasets_coco, merge_datasets_pvoc + + image_dirs = [x[0] for x in args.image_dir] + ann_paths = [x[0] for x in args.ann_path] + + out_dir = args.out_dir + ann_type = args.ann_type + + link_images = args.link_images + + if ann_type == 'COCO': + merge_datasets_coco( + ann_files=ann_paths, + img_dirs=image_dirs, + outdir=out_dir, + link_images=link_images, + ) + elif ann_type == 'PVOC': + merge_datasets_pvoc( + ann_dirs=ann_paths, + img_dirs=image_dirs, + outdir=out_dir, + link_images=link_images, + ) + +def utils_cleanup(args): + '''utils_cleanup + + GinJinn utils cleanup command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils cleanup + subcommand. + ''' + project_dir = args.project_dir + + eval_res_path = os.path.join(project_dir, 'evaluation.csv') + if os.path.exists(eval_res_path): + os.remove(eval_res_path) + print(f'Removed "{eval_res_path}" ...') + + class_names_path = os.path.join(project_dir, 'class_names.txt') + if os.path.exists(class_names_path): + os.remove(class_names_path) + print(f'Removed "{class_names_path}" ...') + + outputs_path = os.path.join(project_dir, 'outputs') + if os.path.isdir(outputs_path): + shutil.rmtree(outputs_path) + os.mkdir(outputs_path) + print(f'Cleaned up "{outputs_path}" ...') + + print(f'Project "{project_dir}" cleaned up.') + +def utils_flatten(args): + '''utils_flatten + + GinJinn utils flatten command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils flatten + subcommand. + ''' + + out_dir = args.out_dir + image_root_dir = args.image_root_dir + ann_path = args.ann_path + sep = args.seperator + custom_id = args.custom_id + annotated_only = args.annotated_only + + if os.path.exists(out_dir): + if confirmation_cancel( + f'\nDirectory "{out_dir}" already exists.\nDo you want to overwrite it? ' + \ + f'WARNING: this will delete "{out_dir}" and ALL SUBDIRECTORIES!\n' + ): + shutil.rmtree(out_dir) + else: + sys.exit() + + os.mkdir(out_dir) + + flatten_coco( + ann_file=ann_path, + img_root_dir=image_root_dir, + out_dir=out_dir, + sep=sep, + custom_id=custom_id, + annotated_only=annotated_only, + ) + + print(f'Flattened data set written to {out_dir}.') + +def utils_crop(args): + '''utils_crop + + GinJinn utils crop command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils crop + subcommand. + ''' + + from ginjinn.utils import crop_seg_from_coco + from ginjinn.utils import crop_bbox_from_coco + from ginjinn.utils.utils import find_img_dir, ImageDirNotFound + + img_dir = args.img_dir + ann_path = args.ann_path + + if not img_dir: + try: + img_dir = find_img_dir(ann_path) + except ImageDirNotFound: + print( + f'ERROR: could not find "images" folder as sibling of "{ann_path}". Make sure ' +\ + f'there is an "images" folder in the same directory as "{ann_path}" or ' +\ + 'explicitly pass "--image_dir".' + ) + sys.exit() + + if args.type == 'segmentation': + crop_seg_from_coco( + ann_file=ann_path, + img_dir=img_dir, + outdir=args.out_dir, + padding=args.padding, + ) + else: + crop_bbox_from_coco( + ann_file=ann_path, + img_dir=img_dir, + outdir=args.out_dir, + padding=args.padding, + ) + + print( + f'Cropped dataset written to "{args.out_dir}".' + ) + +def utils_sliding_window(args): + '''utils_sliding_window + + GinJinn utils sliding_window command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + sliding_window subcommand. + ''' + window_size = args.window_size + if len(window_size) == 1: + win_width = window_size[0] + win_height = window_size[0] + elif len(window_size) == 2: + win_width = window_size[0] + win_height = window_size[1] + else: + print('ERROR: "-s/--window_size" must receive 1 or 2 arguments.') + return + + overlap = args.overlap + if len(overlap) == 1: + hor_overlap = overlap[0] + vert_overlap = overlap[0] + elif len(overlap) == 2: + hor_overlap = overlap[0] + vert_overlap = overlap[1] + else: + print('ERROR: "-p/--overlap" must receive 1 or 2 arguments.') + return + + from ginjinn.utils.dataset_cropping import sliding_window_crop_coco, sliding_window_crop_pvoc + from ginjinn.utils.utils import get_anntype, find_img_dir, ImageDirNotFound + + if os.path.exists(args.out_dir): + msg = f'Directory "{args.out_dir} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{args.out_dir}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(args.out_dir) + os.mkdir(args.out_dir) + else: + os.mkdir(args.out_dir) + + img_dir_out = os.path.join(args.out_dir, 'images') + if os.path.exists(img_dir_out): + msg = f'Directory "{img_dir_out} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{img_dir_out}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(img_dir_out) + os.mkdir(img_dir_out) + else: + os.mkdir(img_dir_out) + + ann_path = args.ann_path + ann_type = args.ann_type + img_dir = args.img_dir + + if not img_dir: + try: + img_dir = find_img_dir(ann_path) + except ImageDirNotFound: + print( + f'ERROR: could not find "images" folder as sibling of "{ann_path}". Make sure ' +\ + f'there is an "images" folder in the same directory as "{ann_path}" or ' +\ + 'explicitly pass "--image_dir".' + ) + sys.exit() + + if ann_type == 'auto': + ann_type = get_anntype(ann_path) + + if ann_type == 'COCO': + ann_path_out = os.path.join(args.out_dir, 'annotations.json') + + sliding_window_crop_coco( + img_dir=img_dir, + ann_path=ann_path, + img_dir_out=img_dir_out, + ann_path_out=ann_path_out, + win_width=win_width, + win_height=win_height, + hor_overlap=hor_overlap, + vert_overlap=vert_overlap, + img_id=args.img_id, + obj_id=args.obj_id, + save_empty=not args.remove_empty, + keep_incomplete=not args.remove_incomplete, + task=args.task, + ) + + msg = f'Sliding-window cropped images written to {img_dir_out}. '+\ + f'Sliding-window cropped annotation written to {ann_path_out}.' + print(msg) + + elif ann_type == 'PVOC': + ann_dir_out = os.path.join(args.out_dir, 'annotations') + if os.path.exists(ann_dir_out): + msg = f'Directory "{ann_dir_out} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{ann_dir_out}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(ann_dir_out) + os.mkdir(ann_dir_out) + else: + os.mkdir(ann_dir_out) + + sliding_window_crop_pvoc( + img_dir=img_dir, + ann_dir=ann_path, + img_dir_out=img_dir_out, + ann_dir_out=ann_dir_out, + win_width=win_width, + win_height=win_height, + hor_overlap=hor_overlap, + vert_overlap=vert_overlap, + save_empty=not args.remove_empty, + keep_incomplete=not args.remove_incomplete, + ) + + msg = f'Sliding-window cropped images written to {img_dir_out}. '+\ + f'Sliding-window cropped annotations written to {ann_dir_out}.' + print(msg) + +def utils_sw_images(args): + '''utils_sw_images + + GinJinn utils sw_images command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + sw_images subcommand. + ''' + window_size = args.window_size + if len(window_size) == 1: + win_width = window_size[0] + win_height = window_size[0] + elif len(window_size) == 2: + win_width = window_size[0] + win_height = window_size[1] + else: + print('ERROR: "-s/--window_size" must receive 1 or 2 arguments.') + return + + overlap = args.overlap + if len(overlap) == 1: + hor_overlap = overlap[0] + vert_overlap = overlap[0] + elif len(overlap) == 2: + hor_overlap = overlap[0] + vert_overlap = overlap[1] + else: + print('ERROR: "-p/--overlap" must receive 1 or 2 arguments.') + return + + from ginjinn.utils.dataset_cropping import sliding_window_crop_images + + if os.path.exists(args.out_dir): + msg = f'Directory "{args.out_dir} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{args.out_dir}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(args.out_dir) + os.mkdir(args.out_dir) + else: + os.mkdir(args.out_dir) + + img_dir = args.img_dir + + if not os.path.isdir(img_dir): + msg = f'ERROR: "{img_dir}" is not a directory.' + print(msg) + sys.exit() + + sliding_window_crop_images( + img_dir=img_dir, + img_dir_out=args.out_dir, + win_width=win_width, + win_height=win_height, + hor_overlap=hor_overlap, + vert_overlap=vert_overlap, + ) + + msg = f'Sliding-window cropped images written to {args.out_dir}.' + print(msg) + +def utils_sw_split(args): + '''utils_sw_split + + GinJinn utils utils_sw_split command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + sw_split subcommand. + ''' + window_size = args.window_size + if len(window_size) == 1: + win_width = window_size[0] + win_height = window_size[0] + elif len(window_size) == 2: + win_width = window_size[0] + win_height = window_size[1] + else: + print('ERROR: "-s/--window_size" must receive 1 or 2 arguments.') + return + + overlap = args.overlap + if len(overlap) == 1: + hor_overlap = overlap[0] + vert_overlap = overlap[0] + elif len(overlap) == 2: + hor_overlap = overlap[0] + vert_overlap = overlap[1] + else: + print('ERROR: "-p/--overlap" must receive 1 or 2 arguments.') + return + + from ginjinn.utils.dataset_cropping import sliding_window_crop_coco, sliding_window_crop_pvoc + + if os.path.exists(args.out_dir): + msg = f'Directory "{args.out_dir} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{args.out_dir}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(args.out_dir) + os.mkdir(args.out_dir) + else: + print('sw_split canceled') + return + else: + os.mkdir(args.out_dir) + + + from ginjinn.utils.utils import get_dstype + + ann_type = args.ann_type + # infer ann_type + if ann_type == 'auto': + ds_types = [] + for ds_name in ['train', 'val', 'test']: + ds_path = os.path.join(args.split_dir, ds_name) + if not os.path.isdir(ds_path): + continue + ds_types.append(get_dstype(ds_path)) + if len(set(ds_types)) > 1: + print( + f'ERROR: Found multiple dataset types in "{args.split_dir}". ' +\ + 'The datasets in splitdir must all have the same annotation type (COCO or PVOC).' + ) + sys.exit() + if len(ds_types) < 1: + print( + f'ERROR: Could not find any dataset (train, val, test) in "{args.split_dir}".' + ) + sys.exit() + ann_type = ds_types[0] + + if ann_type == 'COCO': + for ds_name in ['train', 'val', 'test']: + img_dir = os.path.join(args.split_dir, ds_name, 'images') + ann_path = os.path.join(args.split_dir, ds_name, 'annotations.json') + + if not os.path.isdir(img_dir): + print( + f'No image directory found for dataset "{ds_name}". ' +\ + f'(Expected location: "{img_dir}")' + ) + continue + if not os.path.isfile(ann_path): + print( + f'No annotation file found for dataset "{ds_name}". ' +\ + f'(Expected location: "{ann_path}")' + ) + continue + + img_dir_out = os.path.join(args.out_dir, ds_name, 'images') + os.makedirs(img_dir_out, exist_ok=True) + ann_path_out = os.path.join(args.out_dir, ds_name, 'annotations.json') + + print(f'Splitting dataset {ds_name}...') + sliding_window_crop_coco( + img_dir=img_dir, + ann_path=ann_path, + img_dir_out=img_dir_out, + ann_path_out=ann_path_out, + win_width=win_width, + win_height=win_height, + hor_overlap=hor_overlap, + vert_overlap=vert_overlap, + img_id=args.img_id, + obj_id=args.obj_id, + save_empty=not args.remove_empty, + keep_incomplete=not args.remove_incomplete, + task=args.task, + ) + + msg = \ + f'Sliding-window images for dataset {ds_name} written to {img_dir_out}. '+\ + f'Sliding-window annotation for dataset {ds_name} written to {ann_path_out}.' + print(msg) + + elif ann_type == 'PVOC': + for ds_name in ['train', 'val', 'test']: + img_dir = os.path.join(args.split_dir, ds_name, 'images') + ann_dir = os.path.join(args.split_dir, ds_name, 'annotations') + + if not os.path.isdir(img_dir): + print( + f'No image directory found for dataset "{ds_name}". ' +\ + f'(Expected location: "{img_dir}")' + ) + continue + if not os.path.isdir(ann_dir): + print( + f'No annotation directory found for dataset "{ds_name}". ' +\ + f'(Expected location: "{ann_dir}")' + ) + continue + + img_dir_out = os.path.join(args.out_dir, ds_name, 'images') + os.makedirs(img_dir_out, exist_ok=True) + ann_dir_out = os.path.join(args.out_dir, ds_name, 'annotations') + os.makedirs(ann_dir_out, exist_ok=True) + + print(f'Splitting dataset {ds_name}...') + sliding_window_crop_pvoc( + img_dir=img_dir, + ann_dir=ann_dir, + img_dir_out=img_dir_out, + ann_dir_out=ann_dir_out, + win_width=win_width, + win_height=win_height, + hor_overlap=hor_overlap, + vert_overlap=vert_overlap, + save_empty=not args.remove_empty, + keep_incomplete=not args.remove_incomplete, + ) + + msg = \ + f'Sliding-window images for dataset {ds_name} written to {img_dir_out}. '+\ + f'Sliding-window annotations for dataset {ds_name} written to {ann_dir_out}.' + print(msg) + +def utils_sw_merge(args): + '''utils_sw_merge + + GinJinn utils sw_merge command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + sw_merge subcommand. + ''' + + from ginjinn.utils.sliding_window_merging import merge_sliding_window_predictions + + def on_out_dir_exists(out_dir): + return confirmation_cancel( + f'\nDirectory "{out_dir}" already exists.\nDo you want to overwrite it? ' + \ + f'WARNING: this will delete "{out_dir}" and ALL SUBDIRECTORIES!\n' + ) + + def on_img_out_dir_exists(img_out_dir): + return confirmation_cancel( + f'\nDirectory "{img_out_dir}" already exists.\nDo you want to overwrite it? ' + \ + f'WARNING: this will delete "{img_out_dir}" and ALL SUBDIRECTORIES!\n' + ) + + merge_sliding_window_predictions( + img_dir=args.image_dir, + ann_path=args.ann_path, + out_dir=args.out_dir, + task=args.task, + iou_threshold=args.iou_threshold, + ios_threshold=args.ios_threshold, + intersection_threshold=args.intersection_threshold, + on_out_dir_exists=on_out_dir_exists, + on_img_out_dir_exists=on_img_out_dir_exists, + ) + + msg = f'Merging results written to {args.out_dir}.' + print(msg) + +def utils_filter(args): + '''utils_filter + + GinJinn utils filter command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + filter subcommand. + ''' + + from ginjinn.utils.data_prep import filter_categories_coco, filter_categories_pvoc + from ginjinn.utils.utils import get_anntype + + ann_path = args.ann_path + ann_type = args.ann_type + if ann_type == 'auto': + ann_type = get_anntype(ann_path) + + if ann_type == 'COCO': + filter_categories_coco( + ann_file = ann_path, + img_dir = args.img_dir, + out_dir = args.out_dir, + drop = args.filter if args.drop else None, + keep = args.filter if not args.drop else None, + link_images = not args.copy_images, + ) + elif ann_type == 'PVOC': + filter_categories_pvoc( + ann_dir = ann_path, + img_dir = args.img_dir, + out_dir = args.out_dir, + drop = args.filter if args.drop else None, + keep = args.filter if not args.drop else None, + link_images = not args.copy_images, + ) + else: + print(f'Unknown annotation type "{args.ann_type}".') + return + + print(f'Filtered annotations written to "{args.out_dir}".') + +def utils_filter_size(args): + '''utils_filter_size + + GinJinn utils filter_size command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + filter_size subcommand. + ''' + + from ginjinn.utils.data_prep import filter_objects_by_size + + filter_objects_by_size( + ann_file = args.ann_file, + out_file = args.out_file, + task = args.task, + min_width = args.min_width, + min_height = args.min_height, + min_area = args.min_area, + min_fragment_area = args.min_fragment_area, + ) + + print(f'Filtered annotation written to "{args.out_file}".') + +def utils_visualize(args): + '''utils_visualize + + GinJinn utils visualize command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + visualize subcommand. + ''' + + from ginjinn.utils.utils import visualize_annotations + + if os.path.exists(args.out_dir): + msg = f'Directory "{args.out_dir} already exists. Should it be overwritten?"\n' +\ + f'WARNING: This will remove "{args.out_dir}" and ALL SUBDIRECTORIES.\n' + should_remove = confirmation_cancel(msg) + if should_remove: + shutil.rmtree(args.out_dir) + os.mkdir(args.out_dir) + else: + os.mkdir(args.out_dir) + + from ginjinn.utils.utils import find_img_dir, ImageDirNotFound, get_anntype + + ann_path = args.ann_path + ann_type = args.ann_type + img_dir = args.img_dir + + if not img_dir: + try: + img_dir = find_img_dir(ann_path) + except ImageDirNotFound: + print( + f'ERROR: could not find "images" folder as sibling of "{ann_path}". Make sure ' +\ + f'there is an "images" folder in the same directory as "{ann_path}" or ' +\ + 'explicitly pass "--img_dir".' + ) + sys.exit() + + if ann_type == 'auto': + ann_type = get_anntype(ann_path) + + visualize_annotations( + ann_path = ann_path, + img_dir = img_dir, + out_dir = args.out_dir, + ann_type = ann_type, + vis_type = args.vis_type, + ) + + print(f'Visualizations written to "{args.out_dir}".') + +def utils_count(args): + '''utils_count + + GinJinn utils count command. + + Parameters + ---------- + args + Parsed GinJinn commandline arguments for the ginjinn utils + count subcommand. + ''' + + from ginjinn.utils.utils import count_categories, count_categories_pvoc + + if os.path.isdir(args.ann_path): + count_df = count_categories_pvoc(args.ann_path) + else: + count_df = count_categories(args.ann_path) + count_df.to_csv(args.out_file, index_label='image') + + print(f'\nCategory counts written to {args.out_file}.') diff --git a/ginjinn/data/example_data.txt b/ginjinn/data/example_data.txt new file mode 100644 index 0000000..c35fabc --- /dev/null +++ b/ginjinn/data/example_data.txt @@ -0,0 +1 @@ +This is an example data file. diff --git a/ginjinn/data/ginjinn_config/example_config_0.yaml b/ginjinn/data/ginjinn_config/example_config_0.yaml new file mode 100644 index 0000000..611b654 --- /dev/null +++ b/ginjinn/data/ginjinn_config/example_config_0.yaml @@ -0,0 +1,71 @@ +project_dir: "." +task: "bbox-detection" +# Input data options +input: + type: "PVOC" + training: + annotation_path: "annotations_xyz" + image_path: "images_xyz" +# Model options +model: + name: "faster_rcnn_R_50_FPN_3x" + weights: "pretrained" + model_parameters: + roi_heads: + batch_size_per_image: 4096 +# Options for model training +training: + learning_rate: 0.000125 + batch_size: 1 + max_iter: 40000 + eval_period: 4000 +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + - rotation_range: + angle_min: -45 + angle_max: 45 + expand: True + probability: 0.2 + - rotation_choice: + angles: + - -20 + - -10 + - 10 + - 20 + expand: False + probability: 0.2 + - brightness: + brightness_min: 0.5 + brightness_max: 1.5 + probability: 0.9 + - contrast: + contrast_min: 0.5 + contrast_max: 1.5 + probability: 0.9 + - saturation: + saturation_min: 0.5 + saturation_max: 1.5 + probability: 0.9 + - crop_relative: + width: 0.8 + height: 0.8 + probability: 0.5 + - crop_absolute: + width: 128 + height: 128 + probability: 0.5 + +# Additional options +options: + n_threads: 1 + device: "cuda" + resume: False +# Detectron options +detectron: + MODEL: + ROI_HEADS: + BATCH_SIZE_PER_IMAGE: 256 \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/example_config_1.yaml b/ginjinn/data/ginjinn_config/example_config_1.yaml new file mode 100644 index 0000000..79286f1 --- /dev/null +++ b/ginjinn/data/ginjinn_config/example_config_1.yaml @@ -0,0 +1,70 @@ +project_dir: "." +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "annotations_xyz.json" + image_path: "images_xyz" +# Model options +model: + name: "mask_rcnn_R_50_FPN_3x" + weights: "pretrained" + model_parameters: + roi_heads: + batch_size_per_image: 256 +# Options for model training +training: + learning_rate: 0.000125 + batch_size: 1 + max_iter: 100 + eval_period: 50 +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + - rotation_range: + angle_min: -45 + angle_max: 45 + expand: True + probability: 0.2 + - rotation_choice: + angles: + - -20 + - -10 + - 10 + - 20 + expand: False + probability: 0.2 + - brightness: + brightness_min: 0.5 + brightness_max: 1.5 + probability: 0.9 + - contrast: + contrast_min: 0.5 + contrast_max: 1.5 + probability: 0.9 + - saturation: + saturation_min: 0.5 + saturation_max: 1.5 + probability: 0.9 + - crop_relative: + width: 0.8 + height: 0.8 + probability: 0.5 + - crop_absolute: + width: 128 + height: 128 + probability: 0.5 + +# Additional options +options: + n_threads: 10 + resume: False +# Detectron options +detectron: + MODEL: + ROI_HEADS: + BATCH_SIZE_PER_IMAGE: 256 \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/template_config.yaml b/ginjinn/data/ginjinn_config/template_config.yaml new file mode 100644 index 0000000..563936d --- /dev/null +++ b/ginjinn/data/ginjinn_config/template_config.yaml @@ -0,0 +1,41 @@ +project_dir: "ENTER PROJECT DIRECTORY" +task: "ENTER TASK" +# Input data options +input: + type: "ENTER ANNOTATION TYPE" + training: + annotation_path: "ENTER TRAINING ANNOATION PATH" + image_path: "ENTER TRAINING IMAGES" + validation: + annotation_path: "ENTER VALIDATION ANNOATION PATH" + image_path: "ENTER VALIDATION IMAGES" + test: + annotation_path: "ENTER TEST ANNOATION PATH" + image_path: "ENTER TEST IMAGES" +# Model options +model: + name: "ENTER MODEL NAME" + weights: "pretrained" +# Options for model training +training: + learning_rate: 0.000125 + batch_size: 1 + max_iter: 40000 +# Options for image augmentation +augmentation: + - brightness: + brightness_min: 0.5 + brightness_max: 1.5 + probability: 0.9 + - contrast: + contrast_min: 0.5 + contrast_max: 1.5 + probability: 0.9 + - saturation: + saturation_min: 0.5 + saturation_max: 1.5 + probability: 0.9 + +# Additional options +options: + n_threads: 1 diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_C4_3x.yaml new file mode 100644 index 0000000..595e439 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_C4_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_C4_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_DC5_3x.yaml new file mode 100644 index 0000000..6660668 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_DC5_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_DC5_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_FPN_3x.yaml new file mode 100644 index 0000000..ec1003f --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_101_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_1x.yaml new file mode 100644 index 0000000..4d4ec43 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_1x.yaml @@ -0,0 +1,84 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_C4_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_3x.yaml new file mode 100644 index 0000000..781cf0f --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_C4_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_C4_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_1x.yaml new file mode 100644 index 0000000..282a1fc --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_1x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_DC5_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_3x.yaml new file mode 100644 index 0000000..2dd6b21 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_DC5_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_DC5_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_1x.yaml new file mode 100644 index 0000000..d518c4b --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_1x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_FPN_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_3x.yaml new file mode 100644 index 0000000..a10c465 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_R_50_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_X_101_32x8d_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_X_101_32x8d_FPN_3x.yaml new file mode 100644 index 0000000..60ab26e --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_faster_rcnn_X_101_32x8d_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_X_101_32x8d_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_C4_3x.yaml new file mode 100644 index 0000000..476a9c5 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_C4_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_C4_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_DC5_3x.yaml new file mode 100644 index 0000000..1f1d6f0 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_DC5_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_DC5_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_FPN_3x.yaml new file mode 100644 index 0000000..eaa7a40 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_101_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_1x.yaml new file mode 100644 index 0000000..8d6b762 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_1x.yaml @@ -0,0 +1,85 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" +# Model options +model: + name: "mask_rcnn_R_50_C4_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_3x.yaml new file mode 100644 index 0000000..6dc7c75 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_C4_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_C4_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_1x.yaml new file mode 100644 index 0000000..22e98b4 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_1x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_C4_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_3x.yaml new file mode 100644 index 0000000..816d002 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_DC5_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_DC5_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_1x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_1x.yaml new file mode 100644 index 0000000..95b50af --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_1x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_FPN_1x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_3x.yaml new file mode 100644 index 0000000..87f9e45 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_R_50_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_X_101_32x8d_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_X_101_32x8d_FPN_3x.yaml new file mode 100644 index 0000000..5557c6e --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/adv_mask_rcnn_X_101_32x8d_FPN_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_X_101_32x8d_FPN_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - - 64 + - - 128 + - - 256 + - - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_C4_3x.yaml new file mode 100644 index 0000000..83d4809 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_C4_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_C4_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_DC5_3x.yaml new file mode 100644 index 0000000..e9db0c9 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_DC5_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_DC5_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_FPN_3x.yaml new file mode 100644 index 0000000..43bedcb --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_101_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_101_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_1x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_1x.yaml new file mode 100644 index 0000000..e7dfc55 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_1x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_C4_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_3x.yaml new file mode 100644 index 0000000..6964bdc --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_C4_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_C4_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_1x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_1x.yaml new file mode 100644 index 0000000..ea301fc --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_1x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_DC5_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_3x.yaml new file mode 100644 index 0000000..1c44979 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_DC5_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_DC5_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_1x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_1x.yaml new file mode 100644 index 0000000..a949303 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_1x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_FPN_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_3x.yaml new file mode 100644 index 0000000..908817c --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_R_50_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_R_50_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/faster_rcnn_X_101_32x8d_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/faster_rcnn_X_101_32x8d_FPN_3x.yaml new file mode 100644 index 0000000..40e5d3b --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/faster_rcnn_X_101_32x8d_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "bbox-detection" +# Input data options +input: + type: "COCO" # or "PVOC" + training: + annotation_path: "ENTER ANNOTATION PATH HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION PATH HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "faster_rcnn_X_101_32x8d_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_C4_3x.yaml new file mode 100644 index 0000000..537aac0 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_C4_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_C4_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_DC5_3x.yaml new file mode 100644 index 0000000..03a7553 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_DC5_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_DC5_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_FPN_3x.yaml new file mode 100644 index 0000000..de07446 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_101_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_101_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_1x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_1x.yaml new file mode 100644 index 0000000..471e87e --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_1x.yaml @@ -0,0 +1,54 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" +# Model options +model: + name: "mask_rcnn_R_50_C4_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_3x.yaml new file mode 100644 index 0000000..6dc7c75 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_C4_3x.yaml @@ -0,0 +1,86 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_C4_3x" + weights: "pretrained" + + # additional model options + model_parameters: + # anchor generator options + anchor_generator: + sizes: + - - 32 + - 64 + - 128 + - 256 + - 512 + aspect_ratios: + - - 0.5 + - 1.0 + - 2.0 + angles: + - - -90 + - 0 + - 90 + # region proposal network options + rpn: + iou_thresholds: + - 0.3 + - 0.7 + batch_size_per_image: 256 + # regions of interest options + roi_heads: + iou_thresholds: + - 0.5 + batch_size_per_image: 512 + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_1x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_1x.yaml new file mode 100644 index 0000000..d2a0b60 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_1x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_C4_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_3x.yaml new file mode 100644 index 0000000..4830b52 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_DC5_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_DC5_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_1x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_1x.yaml new file mode 100644 index 0000000..318265b --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_1x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_FPN_1x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_3x.yaml new file mode 100644 index 0000000..81c7bd2 --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_R_50_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_R_50_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data/ginjinn_config/templates/mask_rcnn_X_101_32x8d_FPN_3x.yaml b/ginjinn/data/ginjinn_config/templates/mask_rcnn_X_101_32x8d_FPN_3x.yaml new file mode 100644 index 0000000..2ec5c4e --- /dev/null +++ b/ginjinn/data/ginjinn_config/templates/mask_rcnn_X_101_32x8d_FPN_3x.yaml @@ -0,0 +1,56 @@ +project_dir: "ENTER PROJECT DIR HERE" +task: "instance-segmentation" +# Input data options +input: + type: "COCO" + training: + annotation_path: "ENTER ANNOTATION FILE HERE" + image_path: "ENTER IMAGE DIR HERE" + validation: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + test: + annotation_path: "ENTER ANNOTATION FILE HERE (OPTIONAL)" + image_path: "ENTER IMAGE DIR HERE (OPTIONAL)" + +# Model options +model: + name: "mask_rcnn_X_101_32x8d_FPN_3x" + weights: "pretrained" + +# Options for model training +training: + learning_rate: 0.00125 + batch_size: 1 + max_iter: 5000 + eval_period: 250 + checkpoint_period: 2500 + +# Options for image augmentation +augmentation: + - horizontal_flip: + probability: 0.25 + - vertical_flip: + probability: 0.25 + # - brightness: + # brightness_min: 0.8 + # brightness_max: 1.2 + # probability: 0.25 + # - contrast: + # contrast_min: 0.8 + # contrast_max: 1.2 + # probability: 0.25 + # - saturation: + # saturation_min: 0.8 + # saturation_max: 1.2 + # probability: 0.25 + # - rotation_range: + # angle_min: -30 + # angle_max: 30 + # expand: True + # probability: 0.25 + +# Additional options +options: + n_threads: 1 + resume: True \ No newline at end of file diff --git a/ginjinn/data_reader/__init__.py b/ginjinn/data_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ginjinn/data_reader/data_error.py b/ginjinn/data_reader/data_error.py new file mode 100644 index 0000000..b60542c --- /dev/null +++ b/ginjinn/data_reader/data_error.py @@ -0,0 +1,10 @@ +'''Module for dataset errors. +''' + +class IncompatibleDatasetsError(Exception): + '''Errors due to incompatible datasets for training, test and validation. + ''' + +class ImproperDatasetError(Exception): + '''Errors due to an improper input dataset. + ''' diff --git a/ginjinn/data_reader/data_reader.py b/ginjinn/data_reader/data_reader.py new file mode 100644 index 0000000..9785b3b --- /dev/null +++ b/ginjinn/data_reader/data_reader.py @@ -0,0 +1,249 @@ +""" +Convert input to Detectron2's default dictionary format. +""" + +import glob +import os +from typing import List +import xml.etree.ElementTree as ET +from detectron2.structures import BoxMode +from pycocotools.coco import COCO +from .data_error import IncompatibleDatasetsError + + +def get_class_names_pvoc(ann_dirs: List[str]) -> List[str]: + """Get all object class names contained in a number of Pascal VOC annotation files. + + Parameters + ---------- + ann_dirs : list of str + Directories containing annotation files, each xml file is scanned. + + Returns + ------- + class_names : list of str + Ordered list of object class names + + Raises + ------ + IncompatibleDatasetsError + If different datasets comprise different sets of class names. + """ + class_names = set() + + for ann_dir in ann_dirs: + if ann_dir is None: + continue + classes_dir = set() + for ann_file in glob.glob(os.path.join(ann_dir, "*.xml")): + tree = ET.parse(ann_file) + root = tree.getroot() + + for obj in root.findall("object"): + name = obj.findtext("name") + classes_dir.add(name) + + class_names.add(tuple(sorted(classes_dir))) + + if len(class_names) > 1: + raise IncompatibleDatasetsError( + "All data sets must contain the same categories." + ) + + class_names = class_names.pop() + + return list(class_names) + + +def get_class_names_coco(ann_files: List[str]) -> List[str]: + """Get all object class names contained in a number of COCO annotation files. + + Parameters + ---------- + ann_files : list of str + List of COCO json files + + Returns + ------- + class_names : list of str + Ordered list of object class names + + Raises + ------ + IncompatibleDatasetsError + If different datasets comprise different sets of class names. + """ + class_names = set() + + for ann_file in ann_files: + if ann_file is None: + continue + coco_api = COCO(ann_file) + category_ids = coco_api.getCatIds() + classes_file = [cat["name"] for cat in coco_api.loadCats(category_ids)] + class_names.add(tuple(sorted(classes_file))) + + if len(class_names) > 1: + raise IncompatibleDatasetsError( + "All data sets must contain the same categories." + ) + + class_names = class_names.pop() + + return list(class_names) + + +def save_class_names( + project_dir: str, + class_names: List[str] + ): + """Save object class names within the project directory. + + Parameters + ---------- + project_dir : str + GinJinn project directory + class_names : list of str + Ordered list of object class names + """ + path = os.path.join(project_dir, "class_names.txt") + with open(path, "w") as fout: + fout.write("\n".join(class_names)) + + +def get_class_names( + project_dir: str + ): + """Get object class names stored in the project directory. + + Parameters + ---------- + project_dir : str + GinJinn project directory + + Returns + ------- + class_names : list of str + Ordered list of object class names + """ + path = os.path.join(project_dir, "class_names.txt") + with open(path, "r") as fin: + class_names = [s.strip() for s in fin.readlines()] + return class_names + + +def set_category_ids_coco(dict_list: List[dict], ann_file: str): + """Sort categories alphabetically and assign contiguous IDs starting from 0. + + Parameters + ---------- + dict_list : list of dict + Annotations in Detectron2 format + ann_file : str + COCO json file + + Returns + ------- + class_names: list of str + Ordered list of object class names + """ + coco_api = COCO(ann_file) + category_ids = coco_api.getCatIds() + class_names = [cat["name"] for cat in coco_api.loadCats(category_ids)] + mapping_orig = dict(zip(category_ids, class_names)) + class_names.sort() + + for record in dict_list: + for obj in record["annotations"]: + id_orig = obj["category_id"] + id_new = class_names.index(mapping_orig[id_orig]) + obj["category_id"] = id_new + + +# unused +def get_img_ids(img_dirs: List[str]) -> dict: + """Assign unique ID to each JPG image from multiple directories. + + Parameters + ---------- + img_dirs : list of str + Directories containing JPG images. + + Returns + ------- + img_ids : dict + key = image path, value = image ID + """ + i_img = 0 + img_ids = {} + + for img_dir in img_dirs: + for img_file in os.scandir(img_dir): + if os.path.splitext(img_file.path)[1] in ["jpg", "jpeg", "JPG", "JPEG"]: + img_ids[img_file.path] = i_img + i_img += 1 + + return img_ids + + +def get_dicts_pvoc(ann_dir: str, img_dir: str, class_names: List[str]) -> List[dict]: + """Load Pascal VOC annotations to Detectron2 format. + + Parameters + ---------- + ann_dir : str + Directory containing xml files + img_dir : str + Directory containing JPG images + class_names : list of str + required to assign category IDs + + Notes + ----- + Bounding boxes contained in the input annotations are assumed to represent zero-based, + half open intervals (xmax and ymax don't belong to the object). + + Returns + ------- + dict_list : list of dict + Annotations in Detectron2 format + """ + dict_list = [] + + for i_img, ann_file in enumerate(glob.glob(os.path.join(ann_dir, "*.xml"))): + + tree = ET.parse(ann_file) + root = tree.getroot() + + record = {} + + # folder = root.findtext("folder") + filename = root.findtext("filename") + record["file_name"] = os.path.join(img_dir, filename) + record["image_id"] = i_img + + size = root.find("size") + record["height"] = int(size.findtext("height")) + record["width"] = int(size.findtext("width")) + + record["annotations"] = [] + + for obj in root.findall("object"): + bbox = obj.find("bndbox") + xmin = float(bbox.findtext("xmin")) + ymin = float(bbox.findtext("ymin")) + xmax = float(bbox.findtext("xmax")) + ymax = float(bbox.findtext("ymax")) + name = obj.findtext("name") + + annotation = { + 'bbox': [xmin, ymin, xmax, ymax], + 'bbox_mode': BoxMode.XYXY_ABS, + 'category_id': class_names.index(name) + } + + record["annotations"].append(annotation) + + dict_list.append(record) + + return dict_list diff --git a/ginjinn/data_reader/data_splitter.py b/ginjinn/data_reader/data_splitter.py new file mode 100644 index 0000000..3837edb --- /dev/null +++ b/ginjinn/data_reader/data_splitter.py @@ -0,0 +1,624 @@ +""" +Functions to generate a class-balanced training/validation/test split. +""" + +import glob +import json +import os +import random +import shutil +import sys +import xml.etree.ElementTree as ET +from typing import List, Union, Generator, Callable, Optional, Tuple +import numpy as np +import pandas as pd +from detectron2.data.datasets import load_coco_json +from ginjinn.utils.utils import confirmation +from .data_reader import get_class_names_coco, set_category_ids_coco +from .data_reader import get_class_names_pvoc, get_dicts_pvoc +from .data_error import ImproperDatasetError + +def create_split( + ann_path: str, + img_path: str, + split_dir: str, + task: str, + ann_type: str, + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0, + return_dicts: bool = False + ): + """Create a new train/val/test split, which is stored in split_dir. + To avoid wasting disk space, images are not copied, but hard-linked into + their new directories. This function may require user interaction. + + Parameters + ---------- + ann_path : str + Annotations to be splitted, either a COCO json file or a directory + containing PascalVOC xml files + img_path : str + Directory containing JPG images + split_dir : str + Directory for storing newly created datasets + task : str + "bbox-detection" or "instance-segmentation" + ann_type : str + "COCO" or "PVOC" + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + return_dicts : bool + If set to True, newly created datasets are not only saved to disk, but also + returned in Detectron2's default dictionary format. + + Returns + ------ + dicts_split : dict + For each subset ("train", "val", "test"), dicts_split[subset] contains + a list of dictionaries, which can be registered as dataset for + Detectron2. If return_dicts=False, None is returned. + """ + if os.path.exists(split_dir): + if confirmation( + split_dir + ' already exists.\nDo you want do overwrite it?' + ): + shutil.rmtree(split_dir) + else: + sys.exit() + + os.makedirs(os.path.join(split_dir, "train", "images")) + if p_val > 0: + os.makedirs(os.path.join(split_dir, "val", "images")) + if p_test > 0: + os.makedirs(os.path.join(split_dir, "test", "images")) + + if ann_type == "COCO": + class_names = get_class_names_coco([ann_path]) + dict_list_all = load_coco_json(ann_path, img_path) + set_category_ids_coco(dict_list_all, ann_path) + dicts_split = split_dataset_dicts(dict_list_all, class_names, task, p_val, p_test) + save_split_coco(ann_path, dicts_split, split_dir) + + if ann_type == "PVOC": + os.mkdir(os.path.join(split_dir, "train", "annotations")) + if p_val > 0: + os.mkdir(os.path.join(split_dir, "val", "annotations")) + if p_test > 0: + os.mkdir(os.path.join(split_dir, "test", "annotations")) + + class_names = get_class_names_pvoc([ann_path]) + dict_list_all = get_dicts_pvoc(ann_path, img_path, class_names) + dicts_split = split_dataset_dicts(dict_list_all, class_names, task, p_val, p_test) + save_split_pvoc(ann_path, dicts_split, split_dir) + + if return_dicts: + return dicts_split + return None + +def split_dataset_dicts( + dict_list: List[dict], + class_names: List[str], + task: str, + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0, +) -> dict: + """Create a new train/val/test split for a list of Detectron2 dataset dictionaries. + This function requires user interaction. + + Parameters + ---------- + dict_list : list of dict + Image annotations in Detectron2's default format + class_names : list of str + Ordered list of object class names + task : str + "bbox-detection" or "instance-segmentation" + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + + + Returns + ------- + dicts_split : dict + For each subset ("train", "val", "test"), dicts_split[subset] contains + a list of dictionaries, which can be registered as dataset for + Detectron2. + """ + class_counts, _ = count_class_occurrences(dict_list, len(class_names), task) + + n_trials = 0 + accept = False + while not accept: + n_trials += 1 + + df, partition = propose_split(class_counts, class_names, p_val, p_test) + print(df) + + # handle invalid splits + if 0 in df.values: + if n_trials < 10: + continue + if confirmation("Could not find a valid split, try again?"): + n_trials = 0 + continue + sys.exit() + + accept = confirmation( + "Do you want to accept this split? (Otherwise a new one will be generated.)" + ) + + dicts_split = dict() + for key in partition: + dicts_split[key] = [d for i, d in enumerate(dict_list) if i in partition[key]] + + return dicts_split + +def create_split_2( + ann_path: str, + img_path: str, + split_dir: str, + task: str, + ann_type: str, + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0, + on_split_dir_exists: Optional[Callable] = lambda x: False, + on_split_proposal: Optional[Callable] = lambda x: True, + on_no_valid_split: Optional[Callable] = lambda: False, +) -> bool: + """Create a new train/val/test split, which is stored in split_dir. + To avoid wasting disk space, images are not copied, but hard-linked into + their new directories. This function may require user interaction. + + Parameters + ---------- + ann_path : str + Annotations to be splitted, either a COCO json file or a directory + containing PascalVOC xml files + img_path : str + Directory containing JPG images + split_dir : str + Directory for storing newly created datasets + task : str + "bbox-detection" or "instance-segmentation" + ann_type : str + "COCO" or "PVOC" + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + on_split_dir_exists : Callable + Function to decide, whether to overwrite existing split_dir. + (str) -> bool + on_split_proposal : Callable + Function to decide, whether to accept a proposed split. + (pd.DataFrame) -> bool + on_split_proposal : Callable + Function to decide, whether to retry splitting after not finding a valid + split proposal. + () -> bool + + Returns + ------ + bool + True if data has been written to disk. + """ + if os.path.exists(split_dir): + if on_split_dir_exists(split_dir): + shutil.rmtree(split_dir) + else: + return False + + os.makedirs(os.path.join(split_dir, "train", "images")) + if p_val > 0: + os.makedirs(os.path.join(split_dir, "val", "images")) + if p_test > 0: + os.makedirs(os.path.join(split_dir, "test", "images")) + + if ann_type == "COCO": + class_names = get_class_names_coco([ann_path]) + dict_list_all = load_coco_json(ann_path, img_path) + set_category_ids_coco(dict_list_all, ann_path) + dicts_split = split_dataset_dicts_2( + dict_list_all, class_names, task, p_val, p_test, + on_split_proposal, on_no_valid_split + ) + save_split_coco(ann_path, dicts_split, split_dir) + + if ann_type == "PVOC": + os.mkdir(os.path.join(split_dir, "train", "annotations")) + if p_val > 0: + os.mkdir(os.path.join(split_dir, "val", "annotations")) + if p_test > 0: + os.mkdir(os.path.join(split_dir, "test", "annotations")) + + class_names = get_class_names_pvoc([ann_path]) + dict_list_all = get_dicts_pvoc(ann_path, img_path, class_names) + dicts_split = split_dataset_dicts_2( + dict_list_all, class_names, task, p_val, p_test, + on_split_proposal, on_no_valid_split + ) + save_split_pvoc(ann_path, dicts_split, split_dir) + + return True + + +def split_dataset_dicts_2( + dict_list: List[dict], + class_names: List[str], + task: str, + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0, + on_split_proposal: Optional[Callable] = lambda x: True, + on_no_valid_split: Optional[Callable] = lambda: False, +) -> dict: + """Create a new train/val/test split for a list of Detectron2 dataset dictionaries. + This function requires user interaction. + + Parameters + ---------- + dict_list : list of dict + Image annotations in Detectron2's default format + class_names : list of str + Ordered list of object class names + task : str + "bbox-detection" or "instance-segmentation" + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + on_split_proposal : Callable + Function to decide, whether to accept a proposed split. + (pd.DataFrame) -> bool + on_no_valid_split : Callable + Function to decide, whether to retry splitting after not finding a valid + split proposal. + () -> bool + + Returns + ------- + dicts_split : dict + For each subset ("train", "val", "test"), dicts_split[subset] contains + a list of dictionaries, which can be registered as dataset for + Detectron2. + """ + class_counts, image_counts = count_class_occurrences(dict_list, len(class_names), task) + + # check input + min_required = 1 + bool(p_val) + bool(p_test) + unsplittable = image_counts < min_required + if any(unsplittable): + raise ImproperDatasetError( + f"\nFor {task}, the following categories are represented by less than {min_required} images:\n"\ + f"{np.array(class_names)[unsplittable].tolist()}\n"\ + "Since every sub-dataset has to comprise objects of each category, it is impossible "\ + "to find a valid split.\nYou may want to remove these categories, which can be done "\ + "using GinJinn's filter utility.\n" + ) + + n_trials = 0 + accept = False + while not accept: + n_trials += 1 + + df, partition = propose_split(class_counts, class_names, p_val, p_test) + + # handle invalid splits + if 0 in df.values: + if n_trials < 10: + continue + if on_no_valid_split(): + n_trials = 0 + continue + sys.exit() + + accept = on_split_proposal(df) + + dicts_split = dict() + for key in partition: + dicts_split[key] = [d for i, d in enumerate(dict_list) if i in partition[key]] + + return dicts_split + +def propose_split( + class_counts: "np.ndarray[np.int]", + class_names: List[str], + p_val: Union[int, float], + p_test: Union[int, float], +) -> "(pd.DataFrame, dict)": + '''propose_split + + Propose a split based on class_counts and p_val, p_test. + + Parameters + ---------- + class_counts : np.ndarray[np.int] + Object class counts. + class_names : List[str] + Names of object classes + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + + Returns + ------- + "pd.DataFrame" + Pandas DataFrame with class counts for the potential split. + ''' + partition = greedy_split(class_counts, p_val, p_test) + + col0 = class_counts[partition["train"], :].sum(axis=0) + col1 = class_counts[partition["val"], :].sum(axis=0) + col2 = class_counts[partition["test"], :].sum(axis=0) + + df1 = pd.DataFrame( + [[len(partition["train"]), len(partition["val"]), len(partition["test"])]], + columns=["train", "val", "test"], + index=["images"] + ) + df2 = pd.DataFrame( + {"train": col0, "val": col1, "test": col2}, + columns=["train", "val", "test"], + index=class_names + ) + df = pd.concat([df1, df2]) + + if p_val == 0: + del df['val'] + if p_test == 0: + del df['test'] + + return df, partition + +def count_class_occurrences( + dict_list: List[dict], + n_classes: int, + task: str = None + ) -> Tuple["np.ndarray[np.int]", "np.ndarray[np.int]"]: + """Count class occurences for each image and vice versa for a Detectron2 dataset. + + Parameters + ---------- + dict_list : list of dict + Image annotations in Detectron2's default format. Note that the category IDs have to be + contiguous, starting from zero. + n_classes : int + Number of object classes + task : str + Unless task=None, objects are only considered if their annotation + contains all information necessary for a specific task (either + "bbox-detection" or "instance-segmentation"). + + Returns + ------- + class_counts : np.ndarray + 2-D array indicating how many objects of each class (column) are + annotated within each image (row) + image_counts : np.ndarray + 1-D array indicating over how many images the objects of each class are distributed + """ + class_counts = np.zeros((len(dict_list), n_classes), dtype=int) + image_counts = np.zeros(n_classes, dtype=int) + + if task == "bbox-detection": + required = ("bbox", "bbox_mode", "category_id") + elif task == "instance-segmentation": + required = ("bbox", "bbox_mode", "category_id", "segmentation") + elif task is None: + required = () + + for i_img, img_annotation in enumerate(dict_list): + contained_classes = np.zeros(n_classes, dtype=int) + # look for (complete) object annotations + for obj_annotation in img_annotation["annotations"]: + if None not in [obj_annotation.get(key) for key in required]: + class_counts[i_img, obj_annotation["category_id"]] += 1 + contained_classes[obj_annotation["category_id"]] = 1 + + image_counts += contained_classes + + return class_counts, image_counts + + +def sel_order( + n: int, + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0 + ) -> Generator[int, None, None]: + """Order in which different data sets select their images. + To avoid possible bias, this function makes sure that the data sets select + their images at regular intervals from the beginning to the end of the + splitting process. + + Parameters + ---------- + n : int + Total number of images + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + + Yields + ------- + j : int + Index of next dataset (0=train, 1=validation, 2=test) + + Raises + ------ + ValueError + If p_val or p_test is not within the allowed range [0, 1] + """ + p = [1-p_val-p_test, p_val, p_test] + + if not (0 <= p_val <= 1 and 0 <= p_test <= 1): + raise ValueError("Both validation and test proportion must be between 0.0 and 1.0.") + + counter = [0] * 3 + + for i_img in range(n): + j = np.argmax([p[i] - counter[i]/(i_img+1) for i in range(3)]) + yield j + counter[j] = counter[j]+1 + + +def greedy_split( + class_counts: "np.ndarray[np.int]", + p_val: Union[int, float] = 0, + p_test: Union[int, float] = 0 + ) -> dict: + """Randomized greedy test split algorithm. + + Parameters + ---------- + class_counts : ndarray + 2-D array indicating how many objects of each class (column) are + annotated within each image (row) + p_val : int or float + Proportion of images to be used as validation set + p_test : int or float + Proportion of images to be used as test set + + Returns + ------- + partition : dict + For each data subset, partition[subset] lists the corresponding image indices, + i.e. row indices in class_counts, + e.g. {'train': [2, 7, 9, 3, 8, 0], 'val': [5, 6], 'test': [4, 1]} + """ + n = class_counts.shape[0] + rest = list(range(n)) + sets = ["train", "val", "test"] + partition = {"train": [], "val": [], "test": []} + class_counts_dataset = {"train": 0, "val": 0, "test": 0} + avg = np.mean(class_counts, axis=0) + + for i_set in sel_order(n, p_val, p_test): + subset = sets[i_set] + best = float("inf") + i_best = rest[0] + k = min(len(rest), 100, n//2) + + # select image + for i in random.sample(range(len(rest)), k): + i_row = rest[i] + new = class_counts_dataset[subset] + class_counts[i_row, :] + cost = np.square(new - avg * (1 + len(partition[subset]))).sum() + if cost < best: + best = cost + i_best = i + partition[subset].append(rest.pop(i_best)) + class_counts_dataset[subset] += class_counts[partition[subset][-1], :] + + return partition + + +def save_split_coco( + ann_file: str, + dicts_split: dict, + split_dir: str + ): + """Save train/val/test split of a COCO dataset to disk. + + This function partitions a dataset with annotations in COCO format into + subsets and stores them within a given directory. New image directories + and new COCO json files are created. To avoid wasting disk space, images + are not copied, but hard-linked into the new directories. + + Parameters + ---------- + ann_file : str + Path of COCO json file to be splitted + dicts_split : dict + For each dataset ("train", "val", "test"), partition[subset] is a + list of image annotations in Detectron2's default dictionary format. + split_dir : str + Directory for storing newly created datasets + """ + with open(ann_file, 'r') as json_file: + ann_dict = json.load(json_file) + + info = ann_dict.get('info') or {} + licenses = ann_dict.get('licenses') or [] + images = ann_dict['images'] + annotations = ann_dict['annotations'] + categories = ann_dict['categories'] + + for key in dicts_split: + if not dicts_split[key]: + continue + + img_dir = os.path.split(dicts_split[key][0]["file_name"])[0] + img_ids = [record["image_id"] for record in dicts_split[key]] + + annotations_part = [ann for ann in annotations if ann["image_id"] in img_ids] + images_part = [img for img in images if img["id"] in img_ids] + + json_new = os.path.join(split_dir, key, "annotations.json") + with open(json_new, 'w') as json_file: + json.dump({ + 'info': info, + 'licenses': licenses, + 'images': images_part, + 'annotations': annotations_part, + 'categories': categories + }, + json_file, + indent=2, # None/0 for more compact representation + sort_keys=True + ) + + img_names_part = [os.path.split(img["file_name"])[1] for img in images_part] + + for fname in img_names_part: + img_path_orig = os.path.join(img_dir, fname) + img_path_new = os.path.join(split_dir, key, "images", fname) + os.link(img_path_orig, img_path_new) + + +def save_split_pvoc( + ann_dir: str, + dicts_split: dict, + split_dir: str + ): + """Save train/val/test split of a PascalVOC dataset to disk. + + This function partitions a dataset with annotations in PascalVOC format + into subsets and stores them within a given directory. New directories for + images and annotation xml files are created. To avoid wasting disk space, + images and annotation files are not copied, but hard-linked into the new + directories. + + Parameters + ---------- + ann_dir : str + Directory containing PascalVOC xml files to be partitioned. + dicts_split : dict + For each dataset ("train", "val", "test"), partition[subset] is a + list of image annotations in Detectron2's default dictionary format. + split_dir : str + Directory for storing newly created datasets + """ + ann_map = dict() + + for ann_path in glob.glob(os.path.join(ann_dir, "*.xml")): + tree = ET.parse(ann_path) + root = tree.getroot() + img_name = os.path.split(root.findtext("filename"))[1] + ann_map[img_name] = ann_path + + for key in dicts_split: + for record in dicts_split[key]: + img_path = record["file_name"] + img_name = os.path.split(img_path)[1] + img_path_new = os.path.join(split_dir, key, "images", img_name) + os.link(img_path, img_path_new) + + ann_path = ann_map[img_name] + ann_name = os.path.split(ann_map[img_name])[1] + ann_path_new = os.path.join(split_dir, key, "annotations", ann_name) + os.link(ann_path, ann_path_new) diff --git a/ginjinn/data_reader/load_datasets.py b/ginjinn/data_reader/load_datasets.py new file mode 100644 index 0000000..9255748 --- /dev/null +++ b/ginjinn/data_reader/load_datasets.py @@ -0,0 +1,205 @@ +""" +Load and register datasets as specified by a GinjinnConfiguration object. +""" + +from typing import List +from detectron2.data import DatasetCatalog, MetadataCatalog +from detectron2.data.datasets import load_coco_json +from ginjinn.ginjinn_config.ginjinn_config import GinjinnConfiguration +from .data_error import IncompatibleDatasetsError +from .data_reader import get_class_names_coco, get_class_names_pvoc +from .data_reader import get_dicts_pvoc, set_category_ids_coco +from .data_reader import get_class_names, save_class_names + + +def register_dicts( + dict_list: List[dict], + dataset_name: str, + class_names: List[str] + ): + """Register list of dictionaries as Detectron2 dataset. + + Parameters + ---------- + dict_list : list of dict + Detectron2 default dictionary format + dataset_name : str + Name of the dataset to be registered + class_names : list of str + Ordered list of object class names + """ + if dict_list: + DatasetCatalog.register(dataset_name, lambda: dict_list) + MetadataCatalog.get(dataset_name).thing_classes = class_names + + +def register_coco( + ann_file: str, + img_dir: str, + dataset_name: str, + class_names: List[str] + ): + """Register dataset with annotations in COCO format in Detectron2. + + Parameters + ---------- + ann_file : str + Path of annotation json file + img_dir : str + Directory containing JPG images + dataset_name : str + Name of the dataset to be registered + class_names : list of str + Ordered list of object class names + """ + if ann_file and img_dir: + dict_list = load_coco_json( + ann_file, + img_dir + ) + set_category_ids_coco( + dict_list, + ann_file + ) + DatasetCatalog.register(dataset_name, lambda: dict_list) + MetadataCatalog.get(dataset_name).thing_classes = class_names + + +def register_pvoc( + ann_dir: str, + img_dir: str, + dataset_name: str, + class_names: List[str] + ): + """Register dataset with annotations in PascalVOC format in Detectron2. + + Parameters + ---------- + ann_dir : str + Directory containing annotation xml files + img_dir : str + Directory containing JPG images + dataset_name : str + Name of the dataset to be registered + class_names : list of str + Ordered list of object class names + """ + if ann_dir and img_dir: + dict_list = get_dicts_pvoc( + ann_dir, + img_dir, + class_names + ) + DatasetCatalog.register(dataset_name, lambda: dict_list) + MetadataCatalog.get(dataset_name).thing_classes = class_names + + +def load_test_set( + cfg: GinjinnConfiguration + ): + """Read and register test dataset. + + Parameters + ---------- + cfg : GinjinnConfiguration + + Raises + ------ + IncompatibleDatasetsError + If the class names contained in the test set do not match those of the training set. + """ + class_names_project = get_class_names(cfg.project_dir) + ann_path = cfg.input.test.annotation_path + img_path = cfg.input.test.image_path + + if cfg.input.type == "COCO": + class_names_test = get_class_names_coco([ann_path]) + if class_names_test != class_names_project: + raise IncompatibleDatasetsError( + "The test set must contain the same categories as the training set." + ) + register_coco(ann_path, img_path, "test", class_names_test) + + elif cfg.input.type == "PVOC": + class_names_test = get_class_names_pvoc([ann_path]) + if class_names_test != class_names_project: + raise IncompatibleDatasetsError( + "The test set must contain the same categories as the training set." + ) + register_pvoc(ann_path, img_path, "test", class_names_test) + +def load_train_val_sets ( + cfg: GinjinnConfiguration + ): + """Read and register datasets for training and, optionally, validation. + + Parameters + ---------- + cfg : GinjinnConfiguration + + Raises + ------ + IncompatibleDatasetsError + If datasets for training and validation comprise different sets of class names. + """ + ann_path_train = cfg.input.train.annotation_path + img_path_train = cfg.input.train.image_path + + if not cfg.input.val is None: + ann_path_val = cfg.input.val.annotation_path + img_path_val = cfg.input.val.image_path + + if cfg.input.type == "COCO": + if not cfg.input.val is None: + class_names = get_class_names_coco([ann_path_train, ann_path_val]) + register_coco(ann_path_train, img_path_train, "train", class_names) + register_coco(ann_path_val, img_path_val, "val", class_names) + else: + class_names = get_class_names_coco([ann_path_train]) + register_coco(ann_path_train, img_path_train, "train", class_names) + + elif cfg.input.type == "PVOC": + if not cfg.input.val is None: + class_names = get_class_names_pvoc([ann_path_train, ann_path_val]) + register_pvoc(ann_path_train, img_path_train, "train", class_names) + register_pvoc(ann_path_val, img_path_val, "val", class_names) + else: + class_names = get_class_names_pvoc([ann_path_train]) + register_pvoc(ann_path_train, img_path_train, "train", class_names) + + save_class_names(cfg.project_dir, class_names) + +def load_vis_set( + ann_path: str, + img_dir: str, + ann_type: str, + ): + """Read and register a visualization ("vis") data set. + The registered data set can be accessed via DatasetCatalog.get('vis'). + + Parameters + ---------- + ann_path: str + Path to annotations JSON file for a COCO data set or path to a directory + containing XML annotations files for a PVOC data set. + img_dir: str + Path to a directory containing images corresponding to annotations in + ann_path. + ann_type: str + Type of annotation. Either "COCO" or "PVOC". + + Raises + ------ + Exception + Raised if an invalid annotation type is passed. + """ + + if ann_type == "COCO": + class_names = get_class_names_coco([ann_path]) + register_coco(ann_path, img_dir, "vis", class_names) + elif ann_type == "PVOC": + class_names = get_class_names_pvoc([ann_path]) + register_pvoc(ann_path, img_dir, "vis", class_names) + else: + msg = f'Unknown annotation type "{ann_type}".' + raise Exception(msg) diff --git a/ginjinn/data_reader/merge_datasets.py b/ginjinn/data_reader/merge_datasets.py new file mode 100644 index 0000000..bbfbf8e --- /dev/null +++ b/ginjinn/data_reader/merge_datasets.py @@ -0,0 +1,260 @@ +""" +Functions for merging multiple datasets. +""" + +import datetime +import glob +import hashlib +import json +import os +import shutil +from typing import List +import xml.etree.ElementTree as ET +from ginjinn.data_reader.data_error import IncompatibleDatasetsError + + +def merge_datasets_pvoc( + ann_dirs: List[str], + img_dirs: List[str], + outdir: str, + link_images: bool=True +): + """Combine multiple datasets with annotations in PascalVOC format. + + Parameters + ---------- + ann_dirs : list of str + Directories containing PascalVOC XML files. + Each of he latter must have a unique file name. + img_dirs : list of str + Directories containing JPG images. + Each of he latter must have a unique file name. + outdir : str + Output directory + link_images : bool + If true, images won't be copied but hard-linked instead. + """ + + # create/clean new annotation directory + ann_outdir = os.path.join(outdir, "annotations") + os.makedirs(ann_outdir, exist_ok=True) + for path in glob.iglob(os.path.join(ann_outdir, "*")): + os.remove(path) + + # create/clean new image directory + img_outdir = os.path.join(outdir, "images") + os.makedirs(img_outdir, exist_ok=True) + for path in glob.iglob(os.path.join(img_outdir, "*")): + os.remove(path) + + # find images + extensions = ("jpg", "jpeg", "JPG", "JPEG") + img_paths = [] + for img_dir in img_dirs: + for ext in extensions: + img_paths.extend(glob.glob(os.path.join(img_dir, "*." + ext))) + + # find annotation files + extensions = ("xml", "XML") + ann_paths = [] + for ann_dir in ann_dirs: + for ext in extensions: + ann_paths.extend(glob.glob(os.path.join(ann_dir, "*." + ext))) + + # check for duplicate images + hashes = dict() + for img_path in img_paths: + with open(img_path, "rb") as f: + h = hashlib.md5(f.read()).hexdigest() + if h in hashes: + raise IncompatibleDatasetsError( + f"Identical images files detected:\n{hashes[h]}\n{img_path}" + ) + else: + hashes[h] = img_path + + # check for colliding image file names + images = dict() + for img_path in img_paths: + img_name = os.path.split(img_path)[1] + if img_name in images: + raise IncompatibleDatasetsError( + f"Identical file names detected:\n{images[img_name]}\n{img_path}" + ) + else: + images[img_name] = img_path + + # check for colliding annotation file names + annotations = dict() + for ann_path in ann_paths: + ann_name = os.path.split(ann_path)[1] + if ann_name in annotations: + raise IncompatibleDatasetsError( + f"Identical file names detected:\n{annotations[ann_name]}\n{ann_path}" + ) + else: + annotations[ann_name] = ann_path + + # write annotations + for ann_dir in ann_dirs: + for ann_file in glob.glob(os.path.join(ann_dir, "*.xml")): + tree = ET.parse(ann_file) + root = tree.getroot() + + filename = root.findtext("filename") + for folder in root.iter("folder"): + folder.text = img_outdir + for path in root.iter("path"): + path.text = os.path.join(img_outdir, filename) + + tree.write(os.path.join(ann_outdir, ann_file)) + + # copy/link images + for img_path in img_paths: + img_name = os.path.split(img_path)[1] + img_path_new = os.path.join(img_outdir, img_name) + if link_images: + os.link(img_path, img_path_new) + else: + shutil.copy(img_path, img_path_new) + + +def merge_datasets_coco( + ann_files: List[str], + img_dirs: List[str], + outdir: str, + link_images: bool=True +): + """Combine multiple datasets with annotations in COCO format. + + Parameters + ---------- + ann_files : list of str + List of COCO json files + img_dirs : list of str + Directories containing JPG images. + Each of he latter must have a unique file name. + outdir : str + Output directory + link_images : bool + If true, images won't be copied but hard-linked instead. + """ + + # create/clean new image directory + img_outdir = os.path.join(outdir, "images") + os.makedirs(img_outdir, exist_ok=True) + for path in glob.iglob(os.path.join(img_outdir, "*")): + os.remove(path) + + # find images + extensions = ("jpg", "jpeg", "JPG", "JPEG") + img_paths = [] + for img_dir in img_dirs: + for ext in extensions: + img_paths.extend(glob.glob(os.path.join(img_dir, "*." + ext))) + + # check for duplicate images + hashes = dict() + for img_path in img_paths: + with open(img_path, "rb") as f: + h = hashlib.md5(f.read()).hexdigest() + if h in hashes: + raise IncompatibleDatasetsError( + f"Identical images files detected:\n{hashes[h]}\n{img_path}" + ) + else: + hashes[h] = img_path + + # check for colliding image file names + images = dict() + for img_path in img_paths: + img_name = os.path.split(img_path)[1] + if img_name in images: + raise IncompatibleDatasetsError( + f"Identical file names detected:\n{images[img_name]}\n{img_path}" + ) + else: + images[img_name] = img_path + + # copy/link images + for img_path in img_paths: + img_name = os.path.split(img_path)[1] + img_path_new = os.path.join(img_outdir, img_name) + if link_images: + os.link(img_path, img_path_new) + else: + shutil.copy(img_path, img_path_new) + + # combine json files: + + info = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + + # name -> COCO entry + dict_licenses = dict() + dict_images = dict() + dict_categories = dict() + + annotations = [] + + for ann_file in ann_files: + with open(ann_file, "rb") as f: + ann = json.load(f) + + id_to_lic = dict() # old mapping + for license in ann.get("licenses"): + id_to_lic[license["id"]] = license["name"] + if license["name"] not in dict_licenses: + dict_licenses[license["name"]] = license + dict_licenses[license["name"]]["id"] = len(dict_licenses) + + id_to_img = dict() # old mapping + for image in ann.get("images"): + id_to_img[image["id"]] = image["file_name"] + dict_images[image["file_name"]] = image + dict_images[image["file_name"]]["id"] = len(dict_images) + license = id_to_lic[image["license"]] + dict_images[image["file_name"]]["license"] = dict_licenses[license]["id"] + + id_to_cat = dict() # old mapping + for category in ann.get("categories"): + id_to_cat[category["id"]] = category["name"] + if category["name"] not in dict_categories: + dict_categories[category["name"]] = category + dict_categories[category["name"]]["id"] = len(dict_categories) + + for annotation in ann.get("annotations"): + annotation["id"] = len(annotations) + 1 + + img_file = id_to_img[annotation["image_id"]] + annotation["image_id"] = dict_images[img_file]["id"] + + category = id_to_cat[annotation["category_id"]] + annotation["category_id"] = dict_categories[category]["id"] + + annotations.append(annotation) + + licenses = sorted(list(dict_licenses.values()), key=lambda d:d["id"]) + images = sorted(list(dict_images.values()), key=lambda d:d["id"]) + categories = sorted(list(dict_categories.values()), key=lambda d:d["id"]) + + # write COCO annotation file + json_new = os.path.join(outdir, "annotations.json") + with open(json_new, 'w') as json_file: + json.dump({ + 'info': info, + 'licenses': licenses, + 'images': images, + 'annotations': annotations, + 'categories': categories + }, + json_file, + indent = 2, + sort_keys = True + ) diff --git a/ginjinn/data_reader/tests/test_data_splitter.py b/ginjinn/data_reader/tests/test_data_splitter.py new file mode 100644 index 0000000..428caa6 --- /dev/null +++ b/ginjinn/data_reader/tests/test_data_splitter.py @@ -0,0 +1,49 @@ +''' Tests for data_splitter.py +''' + +import pytest +import mock +import tempfile +import os +from ginjinn.data_reader import data_splitter +import ginjinn.simulation as simulation + +@pytest.fixture(scope='module', autouse=True) +def tmp_dir(): + tmpdir = tempfile.TemporaryDirectory() + + yield tmpdir.name + + tmpdir.cleanup() + +@pytest.fixture(scope='module') +def simulate_simple(tmp_dir): + sim_dir = os.path.join(tmp_dir, 'simulate_simple') + os.mkdir(sim_dir) + + img_path = os.path.join(sim_dir, 'images') + os.mkdir(img_path) + ann_path = os.path.join(sim_dir, 'annotations.json') + + simulation.generate_simple_shapes_coco( + img_dir=img_path, + ann_file=ann_path, + n_images=20 + ) + + return img_path, ann_path + +def test_split_simple(tmp_dir, simulate_simple): + img_path, ann_path = simulate_simple + + with mock.patch('builtins.input', return_value="yes"): + split_dir = os.path.join(tmp_dir, 'test_split_simple_splitdir_0') + data_splitter.create_split( + ann_path, img_path, split_dir, 'instance-segmentation', 'COCO', 0.2, 0.2 + ) + + split_dir = os.path.join(tmp_dir, 'test_split_simple_splitdir_1') + data_splitter.create_split_2( + ann_path, img_path, split_dir, 'instance-segmentation', 'COCO', 0.2, 0.2 + ) + diff --git a/ginjinn/evaluation/__init__.py b/ginjinn/evaluation/__init__.py new file mode 100644 index 0000000..543eefd --- /dev/null +++ b/ginjinn/evaluation/__init__.py @@ -0,0 +1,4 @@ +''' Ginjinn evaluation module +''' + +from .evaluate import evaluate, evaluate_detectron diff --git a/ginjinn/evaluation/evaluate.py b/ginjinn/evaluation/evaluate.py new file mode 100644 index 0000000..126d326 --- /dev/null +++ b/ginjinn/evaluation/evaluate.py @@ -0,0 +1,87 @@ +''' Evaluation module +''' + +from detectron2.checkpoint import DetectionCheckpointer +from detectron2.config import CfgNode +from detectron2.data import build_detection_test_loader +from detectron2.evaluation import COCOEvaluator, inference_on_dataset +from detectron2.modeling import build_model +from ginjinn.ginjinn_config import GinjinnConfiguration +import os + +def evaluate_detectron( + cfg: CfgNode, + task: str, + dataset: str = "test", + checkpoint_name: str = "model_final.pth", +): + """Evaluate registered test dataset using COCOEvaluator + + Parameters + ---------- + cfg : CfgNode + Detectron2 configuration + task : str + "bbox-detection" or "instance-segmentation" + dataset : str + Name of registered dataset + checkpoint_name : str + Checkpoint name + + Returns + ------- + eval_results : OrderedDict + AP values + """ + cfg.MODEL.WEIGHTS = os.path.join(cfg.OUTPUT_DIR, checkpoint_name) + + model = build_model(cfg) + + checkpointer = DetectionCheckpointer( + model, + save_dir = cfg.OUTPUT_DIR + ) + + checkpointer.resume_or_load(cfg.MODEL.WEIGHTS, resume=False) + + + if task == "bbox-detection": + eval_tasks = ("bbox", ) + if task == "instance-segmentation": + eval_tasks = ("bbox", "segm") + + evaluator = COCOEvaluator( + dataset, + tasks = eval_tasks, + distributed = False, + output_dir = cfg.OUTPUT_DIR + ) + test_loader = build_detection_test_loader(cfg, dataset) + eval_results = inference_on_dataset(model, test_loader, evaluator) + return eval_results + +def evaluate( + cfg: GinjinnConfiguration, + checkpoint_name: str = "model_final.pth", +): + """Evaluate registered test dataset using COCOEvaluator + + Parameters + ---------- + cfg : GinjinnConfiguration + Ginjinn configuration object. + checkpoint_name : str + Checkpoint name. + + Returns + ------- + eval_results : OrderedDict + AP values + """ + + return evaluate_detectron( + cfg.to_detectron2_config(is_test=True), + task = cfg.task, + dataset = "test", + checkpoint_name=checkpoint_name, + ) diff --git a/ginjinn/ginjinn_config/__init__.py b/ginjinn/ginjinn_config/__init__.py new file mode 100644 index 0000000..49bd3ab --- /dev/null +++ b/ginjinn/ginjinn_config/__init__.py @@ -0,0 +1,12 @@ +''' +A module for managing the representation of GinJinn configurations. +''' + +from .ginjinn_config import GinjinnConfiguration +from .input_config import GinjinnInputConfiguration +from .model_config import GinjinnModelConfiguration +from .augmentation_config import GinjinnAugmentationConfiguration +from .config_error import \ + InvalidGinjinnConfigurationError,\ + InvalidInputConfigurationError, \ + InvalidAugmentationConfigurationError diff --git a/ginjinn/ginjinn_config/augmentation_config.py b/ginjinn/ginjinn_config/augmentation_config.py new file mode 100644 index 0000000..e4f5388 --- /dev/null +++ b/ginjinn/ginjinn_config/augmentation_config.py @@ -0,0 +1,922 @@ +''' +GinJinn augmentation configuration module +''' + +from typing import List +import detectron2.data.transforms as T +from .config_error import InvalidAugmentationConfigurationError + +def _check_probability(probability: float): + '''Helper function to check augmentation probabilities + + Parameters + ---------- + probability : float + Augmentation probability + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid probability value is passed. + ''' + if probability < 0.0 or probability > 1.0: + raise InvalidAugmentationConfigurationError( + 'The probability of an augmentation must be between 0.0 and 1.0.' + ) + +class HorizontalFlipAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Horizontal Flip Augmentation Configuration + + Parameters + ---------- + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__(self, probability: float = 1.0): + _check_probability(probability) + + self.probability = probability + + @classmethod + def from_dictionary(cls, config: dict): + '''Build HorizontalFlipAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing horizontal flip configurations. + + Returns + ------- + HorizontalFlipAugmentation + HorizontalFlipAugmentation object. + ''' + probability = config.get('probability', 1.0) + return cls(probability = probability) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomFlip( + prob=self.probability, + horizontal=True, + vertical=False + ) + +class VerticalFlipAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Vertical Flip Augmentation Configuration + + Parameters + ---------- + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__(self, probability: float = 1.0): + _check_probability(probability) + + self.probability = probability + + @classmethod + def from_dictionary(cls, config: dict): + '''Build VerticalFlipAugmentation from dictionary + + Parameters + ---------- + config : dict + Dictionary containing vertical flip configurations. + + Returns + ------- + VerticalFlipAugmentation + VerticalFlipAugmentation object. + ''' + probability = config.get('probability', 1.0) + return cls(probability = probability) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomFlip( + prob=self.probability, + horizontal=False, + vertical=True + ) + +class BrightnessAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Random Brightness Augmentation Configuration + + Parameters + ---------- + brightness_min : float + Relative minimal brightness + brightness_max : float + Relative maximal brightness + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + brightness_min: float, + brightness_max: float, + probability: float = 1.0 + ): + _check_probability(probability) + + self.probability = probability + self.brightness_min = brightness_min + self.brightness_max = brightness_max + self._check_brightness() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build BrightnessAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing brightness configurations. + + Returns + ------- + BrightnessAugmentationConfiguration + BrightnessAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid config is passed. + ''' + probability = config.get('probability', 1.0) + brightness_min = config.get('brightness_min', None) + brightness_max = config.get('brightness_max', None) + if brightness_min is None: + raise InvalidAugmentationConfigurationError( + '"brightness_min" required but not in config dictionary' + ) + if brightness_max is None: + raise InvalidAugmentationConfigurationError( + '"brightness_max" required but not in config dictionary' + ) + + return cls( + brightness_min=brightness_min, + brightness_max=brightness_max, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomBrightness( + intensity_min=self.brightness_min, + intensity_max=self.brightness_max, + ), + prob=self.probability + ) + + def _check_brightness(self): + '''Check brightness values for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if brightness values not valid + ''' + if self.brightness_min <= 0: + raise InvalidAugmentationConfigurationError( + 'brightness_min must greather than 0.' + ) + if self.brightness_max <= 0: + raise InvalidAugmentationConfigurationError( + 'brightness_max must greather than 0.' + ) + + if self.brightness_min > self.brightness_max: + raise InvalidAugmentationConfigurationError( + 'brightness_min must the less than brightness_max' + ) + +class ContrastAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Random Contrast Augmentation Configuration + + Parameters + ---------- + contrast_min : float + Relative minimal contrast + contrast_max : float + Relative maximal contrast + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + contrast_min: float, + contrast_max: float, + probability: float = 1.0 + ): + _check_probability(probability) + + self.probability = probability + self.contrast_min = contrast_min + self.contrast_max = contrast_max + self._check_contrast() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build ContrastAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing contrast configurations. + + Returns + ------- + ContrastAugmentationConfiguration + ContrastAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid config is passed. + ''' + probability = config.get('probability', 1.0) + contrast_min = config.get('contrast_min', None) + contrast_max = config.get('contrast_max', None) + if contrast_min is None: + raise InvalidAugmentationConfigurationError( + '"contrast_min" required but not in config dictionary' + ) + if contrast_max is None: + raise InvalidAugmentationConfigurationError( + '"contrast_max" required but not in config dictionary' + ) + + return cls( + contrast_min=contrast_min, + contrast_max=contrast_max, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomContrast( + intensity_min=self.contrast_min, + intensity_max=self.contrast_max, + ), + prob=self.probability + ) + + def _check_contrast(self): + '''Check contrast values for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if contrast values not valid + ''' + if self.contrast_min <= 0: + raise InvalidAugmentationConfigurationError( + 'contrast_min must greather than 0.' + ) + if self.contrast_max <= 0: + raise InvalidAugmentationConfigurationError( + 'contrast_max must greather than 0.' + ) + + if self.contrast_min > self.contrast_max: + raise InvalidAugmentationConfigurationError( + 'contrast_min must the less than contrast_max' + ) + +class SaturationAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Random Saturation Augmentation Configuration + + Parameters + ---------- + saturation_min : float + Relative minimal saturation + saturation_max : float + Relative maximal saturation + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + saturation_min: float, + saturation_max: float, + probability: float = 1.0 + ): + _check_probability(probability) + + self.probability = probability + self.saturation_min = saturation_min + self.saturation_max = saturation_max + self._check_saturation() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build SaturationAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing saturation configurations. + + Returns + ------- + SaturationAugmentationConfiguration + SaturationAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid config is passed. + ''' + probability = config.get('probability', 1.0) + saturation_min = config.get('saturation_min', None) + saturation_max = config.get('saturation_max', None) + if saturation_min is None: + raise InvalidAugmentationConfigurationError( + '"saturation_min" required but not in config dictionary' + ) + if saturation_max is None: + raise InvalidAugmentationConfigurationError( + '"saturation_max" required but not in config dictionary' + ) + + return cls( + saturation_min=saturation_min, + saturation_max=saturation_max, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomSaturation( + intensity_min=self.saturation_min, + intensity_max=self.saturation_max, + ), + prob=self.probability + ) + + def _check_saturation(self): + '''Check saturation values for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if saturation values not valid + ''' + if self.saturation_min <= 0: + raise InvalidAugmentationConfigurationError( + 'saturation_min must greather than 0.' + ) + if self.saturation_max <= 0: + raise InvalidAugmentationConfigurationError( + 'saturation_max must greather than 0.' + ) + + if self.saturation_min > self.saturation_max: + raise InvalidAugmentationConfigurationError( + 'saturation_min must the less than saturation_max' + ) + +class CropRelativeAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Random Crop Augmentation Configuration + + Parameters + ---------- + width : float + Relative width of crop. + height : float + Relative height of crop. + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + width: float, + height: float, + probability: float = 1.0 + ): + _check_probability(probability) + + self.probability = probability + self.width = width + self.height = height + self._check_wh() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build CropRelativeAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing crop configurations. + + Returns + ------- + CropRelativeAugmentationConfiguration + CropRelativeAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid config is passed. + ''' + probability = config.get('probability', 1.0) + width = config.get('width', None) + height = config.get('height', None) + if width is None: + raise InvalidAugmentationConfigurationError( + '"width" required but not in config dictionary' + ) + if height is None: + raise InvalidAugmentationConfigurationError( + '"height" required but not in config dictionary' + ) + + return cls( + width=width, + height=height, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomCrop( + crop_type='relative', + crop_size=(self.height, self.width), + ), + prob=self.probability + ) + + def _check_wh(self): + '''Check width and height values for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if width or height values not valid + ''' + if self.width <= 0.0 or self.width >= 1.0: + raise InvalidAugmentationConfigurationError( + 'width must between 0.0 and 1.0 (exclusive).' + ) + if self.height <= 0.0 or self.height >= 1.0: + raise InvalidAugmentationConfigurationError( + 'height must between 0.0 and 1.0 (exclusive).' + ) + +class CropAbsoluteAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''Random Crop Augmentation Configuration + + Parameters + ---------- + width : int + Absolute width of crop in pixel. + height : int + Absolute height of crop in pixel. + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + width: int, + height: int, + probability: float = 1.0 + ): + _check_probability(probability) + + self.probability = probability + self.width = width + self.height = height + self._check_wh() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build CropAbsoluteAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing crop configurations. + + Returns + ------- + CropAbsoluteAugmentationConfiguration + CropAbsoluteAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid config is passed. + ''' + probability = config.get('probability', 1.0) + width = config.get('width', None) + height = config.get('height', None) + if width is None: + raise InvalidAugmentationConfigurationError( + '"width" required but not in config dictionary' + ) + if height is None: + raise InvalidAugmentationConfigurationError( + '"height" required but not in config dictionary' + ) + + return cls( + width=width, + height=height, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomCrop( + crop_type='absolute', + crop_size=(self.height, self.width), + ), + prob=self.probability + ) + + def _check_wh(self): + '''Check width and height values for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if width or height values not valid + ''' + if self.width <= 0: + raise InvalidAugmentationConfigurationError( + 'width must be greater than 0.' + ) + if self.height <= 0: + raise InvalidAugmentationConfigurationError( + 'height must be greater than 0.' + ) + +class RotationRangeAugmentationConfiguration(): #pylint: disable=too-few-public-methods + '''Rotation range augmentation + + Rotate randomly in the interval between angle_min and angle_max. + + Parameters + ---------- + angle_min: float + Minimum angle of rotation. + angle_max: float + Maximum angle of rotation. + expand: bool + image should be resized to fit the rotated image, alternatively cropped. + By default True (resized). + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + angle_min: float, + angle_max: float, + expand: bool = True, + probability: float = 1.0, + ): + _check_probability(probability) + + self.angle_min = angle_min + self.angle_max = angle_max + self.expand = expand + self.probability = probability + + self._check_angles() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build RotationRangeAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing rotation configurations. + + Returns + ------- + RotationRangeAugmentationConfiguration + RotationRangeAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if required dictionary field is missing + ''' + probability = config.get('probability', 1.0) + expand = config.get('expand', True) + angle_min = config.get('angle_min', None) + angle_max = config.get('angle_max', None) + + if angle_min is None: + raise InvalidAugmentationConfigurationError( + '"angle_min" required but not in config dictionary' + ) + if angle_max is None: + raise InvalidAugmentationConfigurationError( + '"angle_min" required but not in config dictionary' + ) + + return cls( + angle_min=angle_min, + angle_max=angle_max, + expand=expand, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomRotation( + angle=(self.angle_min, self.angle_max), + expand=self.expand, + sample_style='range' + ), + prob=self.probability + ) + + def _check_angles(self): + '''Check angles for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if angles are not valid + ''' + + if self.angle_min > self.angle_max: + raise InvalidAugmentationConfigurationError( + 'angle_min must the less than angle_max' + ) + +class RotationChoiceAugmentationConfiguration(): #pylint: disable=too-few-public-methods + '''Rotation selection augmentation + + Rotate randomly in the interval between angle_min and angle_max. + + Parameters + ---------- + angles: list + list of angles from which a random one will be chosen for each rotation augmentation. + expand: bool + image should be resized to fit the rotated image, alternatively cropped. + By default True (resized). + probability : float, optional + Probability of applying the augmentation, by default 1.0 (always applied). + ''' + + def __init__( + self, + angles: list, + expand: bool = True, + probability: float = 1.0, + ): + _check_probability(probability) + + self.angles = angles + self.expand = expand + self.probability = probability + + self._check_angles() + + @classmethod + def from_dictionary(cls, config: dict): + '''Build RotationChoiceAugmentationConfiguration from dictionary + + Parameters + ---------- + config : dict + Dictionary containing rotation configurations. + + Returns + ------- + RotationChoiceAugmentationConfiguration + RotationChoiceAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if required dictionary field is missing + ''' + probability = config.get('probability', 1.0) + expand = config.get('expand', True) + angles = config.get('angles', None) + + if angles is None: + raise InvalidAugmentationConfigurationError( + '"angles" required but not in config dictionary' + ) + + return cls( + angles=angles, + expand=expand, + probability = probability + ) + + def to_detectron2_augmentation(self): + '''Convert to Detectron2 augmentation + + Returns + ------- + Augmentation + Detectron2 augmentation + ''' + return T.RandomApply( + T.RandomRotation( + angle=self.angles, + expand=self.expand, + sample_style='choice' + ), + prob=self.probability + ) + + def _check_angles(self): + '''Check angles for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised if angles are not valid + ''' + if len(self.angles) < 1: + raise InvalidAugmentationConfigurationError( + 'There must be at least one angle to chose from.' + ) + +class GinjinnAugmentationConfiguration: #pylint: disable=too-few-public-methods + '''A class representing GinJinn augmentation configurations. + ''' + + AVAILABLE_AUGMENTATIONS = { + 'horizontal_flip': HorizontalFlipAugmentationConfiguration, + 'vertical_flip': VerticalFlipAugmentationConfiguration, + 'rotation_range': RotationRangeAugmentationConfiguration, + 'rotation_choice': RotationChoiceAugmentationConfiguration, + 'brightness': BrightnessAugmentationConfiguration, + 'contrast': ContrastAugmentationConfiguration, + 'saturation': SaturationAugmentationConfiguration, + 'crop_relative': CropRelativeAugmentationConfiguration, + 'crop_absolute': CropAbsoluteAugmentationConfiguration, + } + + def __init__( + self, + augmentations: list + ): + '''Class representing augmentation configurations + + Parameters + ---------- + augmentations : list + List of Augmentation objects. + ''' + + self.augmentations = augmentations + self._check_augmentations() + + @classmethod + def from_dictionaries(cls, augmentation_dicts: List[dict]): + '''Build augmentations configuration from list of dictionaries. + + Each augmentation dictionary should consist of single key naming + the augmentation that should be performed with the a corresponding + value, which is again a dictionary, listing the augmentation options. + + The following is an example for a horizontal flip augmentation dict: + { + 'horizontal_flip': { + probability: 0.25 + } + } + + Parameters + ---------- + augmentation_dicts : list[dict] + List of dictionaries describing augmentations. + + Returns + ------- + GinjinnAugmentationConfiguration + GinjinnAugmentationConfiguration object. + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid augmentation name is passed. + ''' + + augmentations = [] + for aug_dict in augmentation_dicts: + # we expect only 1 key, see from_dictionaries documentation + aug_name = list(aug_dict.keys())[0] + aug_constructor = cls.AVAILABLE_AUGMENTATIONS.get(aug_name, None) + if aug_constructor is None: + raise InvalidAugmentationConfigurationError( + 'Unknown augmentation "{}".'.format(aug_name) + ) + + aug = aug_constructor.from_dictionary(aug_dict[aug_name]) + + augmentations.append(aug) + + return cls(augmentations) + + def to_detectron2_augmentations(self): + '''Convert to Detectron2 augmentation list + + Returns + ------- + Augmentations + A list of Detectron2 augmentations + ''' + augmentations = [] + for aug in self.augmentations: + augmentations.append(aug.to_detectron2_augmentation()) + + return augmentations + + def _check_augmentations(self): + '''Check augmentations for validity + + Raises + ------ + InvalidAugmentationConfigurationError + Raised when an invalid augmentation was found. + ''' + + # nothing to check if there are no augmentations + if len(self.augmentations) == 0: + return + + for aug in self.augmentations: + if not any( + [isinstance(aug, av_aug) for av_aug in self.AVAILABLE_AUGMENTATIONS.values()] + ): + raise InvalidAugmentationConfigurationError( + 'Unknown augmentation class "{}".'.format(type(aug)) + ) diff --git a/ginjinn/ginjinn_config/config_error.py b/ginjinn/ginjinn_config/config_error.py new file mode 100644 index 0000000..7f7240c --- /dev/null +++ b/ginjinn/ginjinn_config/config_error.py @@ -0,0 +1,26 @@ +'''Module for GinJinn configuration errors. +''' + +class InvalidInputConfigurationError(Exception): + '''Error representing invalid input configuration. + ''' + +class InvalidModelConfigurationError(Exception): + '''Error representing invalid model configuration. + ''' + +class InvalidAugmentationConfigurationError(Exception): + '''Error representing invalid augmentation configuration. + ''' + +class InvalidGinjinnConfigurationError(Exception): + '''Error representing invalid general GinJinn configuration. + ''' + +class InvalidOptionsConfigurationError(Exception): + '''Error representing invalid general GinJinn configuration. + ''' + +class InvalidTrainingConfigurationError(Exception): + '''Error representing invalid general GinJinn configuration. + ''' diff --git a/ginjinn/ginjinn_config/detectron_config.py b/ginjinn/ginjinn_config/detectron_config.py new file mode 100644 index 0000000..dd0096f --- /dev/null +++ b/ginjinn/ginjinn_config/detectron_config.py @@ -0,0 +1,76 @@ +''' +Detectron2 configuration module +''' + +# import copy +# from typing import Optional + +import collections.abc + +#source: https://stackoverflow.com/a/3233356/5665958 +def update(a, b): + '''update + + Update mappable object a with mappable object b. + + Parameters + ---------- + a + Mappable object (e.g. dict) + b + Mappable object (e.g. dict) + + Returns + ------- + mappable + Updated object a. + ''' + for key, val in b.items(): + if isinstance(val, collections.abc.Mapping): + a[key] = update(a.get(key, {}), val) + else: + a[key] = val + return a + +class GinjinnDetectronConfiguration: #pylint: disable=too-few-public-methods + '''A class representing additional Detectron2 configurations + + Parameters + ---------- + config : dict, optional + A dictionary describing additional Detectron2 configurations, by default {} + ''' + + def __init__(self, config: dict = {}): #pylint: disable=dangerous-default-value + self.config = config + + @classmethod + def from_dictionary(cls, config: dict): + '''Build GinjinnAugmentationConfiguration from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the augmentation configuration. + + Returns + ------- + GinjinnDetectronConfiguration + GinjinnDetectronConfiguration constructed with the configuration + given in config. + ''' + + return cls(config) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the detectron2 extra configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + ''' + + update(cfg, self.config) diff --git a/ginjinn/ginjinn_config/ginjinn_config.py b/ginjinn/ginjinn_config/ginjinn_config.py new file mode 100644 index 0000000..4c0816b --- /dev/null +++ b/ginjinn/ginjinn_config/ginjinn_config.py @@ -0,0 +1,206 @@ +''' +A module for managing the representation of GinJinn configurations. +''' + + +# import copy +# from typing import Optional +import yaml +import os +from .config_error import InvalidGinjinnConfigurationError +from .input_config import GinjinnInputConfiguration +from .model_config import GinjinnModelConfiguration, MODELS +from .augmentation_config import GinjinnAugmentationConfiguration +from .detectron_config import GinjinnDetectronConfiguration +from .options_config import GinjinnOptionsConfiguration +from .training_config import GinjinnTrainingConfiguration + +TASKS = [ + 'bbox-detection', + # 'semantic-segmentation', + 'instance-segmentation', +] + +class GinjinnConfiguration: #pylint: disable=too-many-arguments,too-many-instance-attributes + '''GinJinn configuration class. + + A class representing the configuration of a GinJinn project. + + Parameters + ---------- + project_dir : str + Project directory. All outputs will be written to this directory. + task : str + Object detection task type. + input_configuration : GinjinnInputConfiguration + Object describing the input. + model_configuration : GinjinnModelConfiguration + Object describing the model. + training_configuration : GinjinnTrainingConfiguration + Object desribing the training. + augmentation_configuration : GinjinnAugmentationConfiguration + Object describing the augmentation. + detectron_configuration : GinjinnDetectronConfiguration + Object describing additional detectron2 configurations. + Only use this option if you know what you are doing + options_configuration: GinjinnOptionsConfiguration + Object describing additional GinJinn options. + + Raises + ------ + InvalidGinjinnConfigurationError + If any of the general configuration is contradictionary or malformed. + ''' + def __init__( + self, + project_dir: str, + task: str, + input_configuration: GinjinnInputConfiguration, + model_configuration: GinjinnModelConfiguration, + training_configuration: GinjinnTrainingConfiguration, + augmentation_configuration: GinjinnAugmentationConfiguration, + detectron_configuration: GinjinnDetectronConfiguration = GinjinnDetectronConfiguration(), + options_configuration: GinjinnOptionsConfiguration = + GinjinnOptionsConfiguration.from_dictionary({}), + ): + self.project_dir = project_dir + self.task = task + self.input = input_configuration + self.model = model_configuration + self.training = training_configuration + self.augmentation = augmentation_configuration + self.detectron_config = detectron_configuration + self.options = options_configuration + + self._check() + + def to_detectron2_config(self, is_test: bool = False): + '''to_detectron2_config + + Convert GinJinn configuration to Detectron2 configuration. + + Parameters + ---------- + is_test: bool + Whether current function call is in context of a test setting. + + Returns + ------- + detectron2_config + Detectron2 configuration. + ''' + + # model + config = self.model.to_detectron2_config() + + # input + self.input.update_detectron2_config(config, is_test=is_test) + + # TODO: + # training + self.training.update_detectron2_config(config) + # options, TODO: implement additional options + self.options.update_detectron2_config(config) + # extra detectron config TODO + self.detectron_config.update_detectron2_config(config) + + # detectron2 output dir + config.OUTPUT_DIR = os.path.join(self.project_dir, 'outputs') + + # maybe remove this + print(config) + + return config + + @classmethod + def from_dictionary(cls, config: dict): + '''Build GinjinnConfiguration from dictionary. + + Parameters + ---------- + config : dict + Dictionary object describing the GinJinn configuration. + + Returns + ------- + GinjinnConfiguration + GinjinnConfiguration constructed with the configuration + given in config. + ''' + + project_dir = config['project_dir'] + + input_configuration = GinjinnInputConfiguration.from_dictionary( + config['input'], + project_dir=project_dir, + ) + model_configuration = GinjinnModelConfiguration.from_dictionary( + config['model'] + ) + training_configuration = GinjinnTrainingConfiguration.from_dictionary( + config.get('training', {}) + ) + augmentation_configuration = GinjinnAugmentationConfiguration.from_dictionaries( + config.get('augmentation', []) + ) + detectron_configuration = GinjinnDetectronConfiguration.from_dictionary( + config.get('detectron', {}) + ) + options_configuration = GinjinnOptionsConfiguration.from_dictionary( + config.get('options', {}) + ) + + return cls( + project_dir=project_dir, + task=config['task'], + input_configuration=input_configuration, + model_configuration=model_configuration, + training_configuration=training_configuration, + augmentation_configuration=augmentation_configuration, + detectron_configuration=detectron_configuration, + options_configuration=options_configuration, + ) + + @classmethod + def from_config_file(cls, file_path: str): + '''Build GinjinnConfiguration from YAML configuration file. + + Parameters + ---------- + file_path : str + Path to GinJinn YAML configuration file. + + Returns + ------- + GinjinnConfiguration + GinjinnConfiguration constructed with the configuration + given in the config file. + ''' + + with open(file_path) as config_file: + config = yaml.safe_load(config_file) + + return cls.from_dictionary(config) + + def _check(self): + '''_check + + Check validity of configuration options. + + Raises + ------ + InvalidGinjinnConfigurationError + Raised if invalid task passed. + InvalidGinjinnConfigurationError + Raised if task incompatible with model. + ''' + if not self.task in TASKS: + raise InvalidGinjinnConfigurationError( + '"task" must be one of {}'.format(TASKS) + ) + + model_tasks = MODELS[self.model.name]['tasks'] + if not self.task in model_tasks: + err_msg = f'Task "{self.task}" is incompatible with model ' +\ + f'"{self.model.name}" (available tasks: {", ".join(model_tasks)}).' + raise InvalidGinjinnConfigurationError(err_msg) diff --git a/ginjinn/ginjinn_config/input_config.py b/ginjinn/ginjinn_config/input_config.py new file mode 100644 index 0000000..eace32a --- /dev/null +++ b/ginjinn/ginjinn_config/input_config.py @@ -0,0 +1,356 @@ +''' +GinJinn input configuration module +''' + +import copy +import os +from typing import Optional +from .config_error import InvalidInputConfigurationError + +ANNOTATION_TYPES = [ + 'PVOC', + 'COCO' +] + +class InputPaths: #pylint: disable=too-few-public-methods + '''Class representing annotation and corresponding image paths. + + Parameters + ---------- + ann_path : str + Path to annotations. I.e. either a file or a folder path. + img_path : str + Path to the folder containing images. + ''' + def __init__( + self, + ann_path: str, + img_path: str, + ): + self.annotation_path = ann_path + self.image_path = img_path + +# TODO recycle this for the commandline script +class SplitConfig: #pylint: disable=too-few-public-methods + '''Class representing test and validation split options. + + Parameters + ---------- + test_split : float + Fraction of data set to use for testing. + validation_split : float + Fraction of data set to use for validation. + ''' + def __init__( + self, + test_split: Optional[float] = None, + validation_split: Optional[float] = None, + ): + self.test = test_split + self.val = validation_split + + self._check() + + def _check(self): + ''' Checks validity of splitting. + + Raises + ------ + InvalidInputConfigurationError + Raised in case of invalid splitting options. + ''' + if not self.test is None: + if self.test <= 0.0 or self.test >= 1.0: + raise InvalidInputConfigurationError( + 'The proportion of the test split must be greater than 0.0 and less than 1.0.' #pylint: disable=line-too-long + ) + if not self.val is None: + if self.val <= 0.0 or self.val >= 1.0: + raise InvalidInputConfigurationError( + 'The proportion of the validation split must be greater than 0.0 and less than 1.0.' #pylint: disable=line-too-long + ) + if (not self.test is None) and (not self.val is None): + proportion = self.test + self.val + if proportion >= 1.0 or proportion <= 0.0: + raise InvalidInputConfigurationError( + 'The sum of test and validation split proportions must be greater than 0.0 and less than 1.0.' #pylint: disable=line-too-long + ) + +class GinjinnInputConfiguration: #pylint: disable=too-few-public-methods + '''GinJinn input configuration class. + + A class representing the configuration of the input(s) + for a GinJinn project. This includes the configuration + or description of optionals train-validation-test + splits of the data set. + + Train-validation-test can be + - skipped, when leaving test_* and val_* arguments at default + - custom, when specifying test_* and val_* arguments + + Parameters + ---------- + ann_type : str + Type of the object detection annotations. + "PascalVOC" or "COCO". + train_ann_path : str + Path to the directory containing annotations files for "PascalVOC". + Path to the annotation file for "COCO". + train_img_path : str + Path to the directory containing the images. + test_ann_path : Optional[str], optional + Path to the directory containing annotations files for "PascalVOC". + Path to the annotation file for "COCO". + test_img_path : Optional[str], optional + Path to the directory containing the images. + val_ann_path : Optional[str], optional + Path to the directory containing annotations files for "PascalVOC". + Path to the annotation file for "COCO". + val_img_path : Optional[str], optional + Path to the directory containing the images. + project_dir : str + GinJinn project directory. + + Raises + ------ + InvalidInputConfigurationError + If the input configuration is contradictionary or malformed. + ''' + + def __init__( #pylint: disable=too-many-arguments + self, + ann_type: str, + train_ann_path: str, + train_img_path: str, + test_ann_path: Optional[str] = None, + test_img_path: Optional[str] = None, + val_ann_path: Optional[str] = None, + val_img_path: Optional[str] = None, + project_dir: str = '', + ): + self.project_dir = project_dir + self.type = ann_type + self.train = InputPaths( + self._rel_to_project(train_ann_path), + self._rel_to_project(train_img_path) + ) + + self.test = None + self.val = None + + + + # type + if not self.type in ANNOTATION_TYPES: + raise InvalidInputConfigurationError( + '"ann_type" must be one of {}.'.format(ANNOTATION_TYPES) + ) + + # test + if (not test_ann_path is None) or (not test_img_path is None): + if (test_ann_path is None) or (test_img_path is None): + raise InvalidInputConfigurationError( + 'If any of "test_ann_path" and "test_img_path" is passed, ' \ + 'the other must be passed too.' + ) + self.test = InputPaths( + self._rel_to_project(test_ann_path), + self._rel_to_project(test_img_path), + ) + + # validation + if (not val_ann_path is None) or (not val_img_path is None): + if (val_ann_path is None) or (val_img_path is None): + raise InvalidInputConfigurationError( + 'If any of "val_ann_path" and "val_img_path" is passed, ' \ + 'the other must be passed too.' + ) + self.val = InputPaths( + self._rel_to_project(val_ann_path), + self._rel_to_project(val_img_path), + ) + + # check for file path validity + # TODO: think about whether this should be checked here or later + # in the data reader. + self._check_filepaths() + + def update_detectron2_config(self, cfg, is_test: bool=False): + '''update_detectron2_config + + Updates detectron2 config with the input configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + is_test: bool + Whether current function call is in context of a test setting. + ''' + if not is_test: + if self.train: + cfg.DATASETS.TRAIN = ('train', ) + if self.val: + cfg.DATASETS.TEST = ('val', ) + else: + cfg.DATASETS.TEST = () + else: + if self.test: + cfg.DATASETS.TEST = ('test', ) + + @staticmethod + def _check_pvoc_annotation_path(ann_path: str): + ''' Check for PVOC annotation path validity, else raise an exception + + Parameters + ---------- + ann_path : str + Path to a directory containing annotations. + + Raises + ------ + InvalidInputConfigurationError + This exception is raised if the annotation path is not valid. + ''' + if not os.path.isdir(ann_path): + raise InvalidInputConfigurationError( + '"{}" is not a valid PVOC annotation path. The path might not exist ' \ + 'or refer to a file instead of a directory.'.format(ann_path) + ) + + @staticmethod + def _check_coco_annotation_path(ann_path: str): + ''' Check for COCO annotation path validity, else raise an exception + + Parameters + ---------- + ann_path : str + Path to an annotation JSON file. + + Raises + ------ + InvalidInputConfigurationError + This exception is raised if the annotation path is not valid. + ''' + + if not os.path.isfile(ann_path): + raise InvalidInputConfigurationError( + '"{}" is not a valid COCO annotation file path. The path might not exist ' \ + 'or refer to a directory instead of a file.'.format(ann_path) + ) + + @staticmethod + def _check_image_path(image_path: str): + ''' Check for image path validity, else raise an exception + + Parameters + ---------- + image_path : str + Path to a directory containing images. + + Raises + ------ + InvalidInputConfigurationError + This exception is raised if the image path is not valid. + ''' + if not os.path.isdir(image_path): + raise InvalidInputConfigurationError( + '"{}" is not a valid image directory path. The path might not exist ' \ + 'or refer to a file.'.format(image_path) + ) + + def _check_filepaths(self): + '''Check, whether file path configuration is valid + ''' + + # check for correct annotation type, i.e. files or folders + if self.type == 'PVOC': + self._check_pvoc_annotation_path(self.train.annotation_path) + if not self.test is None: + self._check_pvoc_annotation_path(self.test.annotation_path) + if not self.val is None: + self._check_pvoc_annotation_path(self.val.annotation_path) + elif self.type == 'COCO': + self._check_coco_annotation_path(self.train.annotation_path) + if not self.test is None: + self._check_coco_annotation_path(self.test.annotation_path) + if not self.val is None: + self._check_coco_annotation_path(self.val.annotation_path) + + # check if image directory exists + self._check_image_path(self.train.image_path) + if not self.test is None: + self._check_image_path(self.test.image_path) + if not self.val is None: + self._check_image_path(self.val.image_path) + + @classmethod + def from_dictionary(cls, config: dict, project_dir: str =''): + '''Build GinjinnInputConfiguration from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the input configuration. + project_dir : str + GinJinn project directory. + Returns + ------- + GinjinnInputConfiguration + GinjinnInputConfiguration constructed with the configuration + given in config. + ''' + + default_config = { + 'test': { + 'annotation_path': None, + 'image_path': None, + }, + 'validation': { + 'annotation_path': None, + 'image_path': None, + }, + 'split': { + 'test': None, + 'validation': None + } + } + + # Maybe implement this more elegantly... + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + ann_type = config['type'], + train_ann_path = config['training']['annotation_path'], + train_img_path = config['training']['image_path'], + test_ann_path = config['test']['annotation_path'], + test_img_path = config['test']['image_path'], + val_ann_path = config['validation']['annotation_path'], + val_img_path = config['validation']['image_path'], + project_dir = project_dir, + ) + + def _rel_to_project(self, file_path: str) -> str: + '''_rel_to_project + + Set root of relative file path to self.project_dir instead + of the current shell root. + + Parameters + ---------- + file_path : str + File path to correct. + + Returns + ------- + str + Corrected file path + ''' + + if os.path.isabs(file_path): + return file_path + + return os.path.abspath( + os.path.join(self.project_dir, file_path) + ) diff --git a/ginjinn/ginjinn_config/model_config.py b/ginjinn/ginjinn_config/model_config.py new file mode 100644 index 0000000..55bc5bd --- /dev/null +++ b/ginjinn/ginjinn_config/model_config.py @@ -0,0 +1,788 @@ +''' +GinJinn model configuration module +''' + +import copy +import os +from typing import List +from .config_error import InvalidModelConfigurationError + +class AnchorGeneratorConfig: #pylint: disable=too-few-public-methods + '''AnchorGeneratorConfig + + Object representing AnchorGenerator model configurations. + + Parameters + ---------- + sizes : List[List[float]] + List of lists of anchor sizes in absolute pixels. + aspect_ratios : List[List[float]] + List of lists of anchor aspect ratios. + angles : List[List[float]] + List of lists of anchor angles. + ''' + def __init__( + self, + sizes : List[List[float]] = None, + aspect_ratios : List[List[float]] = None, + angles : List[List[float]] = None, + ): + self.sizes = sizes + self.aspect_ratios = aspect_ratios + self.angles = angles + + @classmethod + def from_dictionary(cls, config: dict): + '''Build AnchorGeneratorConfig from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the AnchorGenerator configuration. + + Returns + ------- + AnchorGeneratorConfig + AnchorGeneratorConfig constructed with the parameters in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown parameter in config dict. + ''' + + available_configs = ['sizes', 'aspect_ratios', 'angles'] + for cfg_name in config.keys(): + if not cfg_name in available_configs: + err_msg = f'Unknown anchor generator parameter name "{cfg_name}". ' +\ + f'Available parameters are {available_configs}.' + raise InvalidModelConfigurationError(err_msg) + + default_config = { + } + + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + sizes=config.get('sizes', None), + aspect_ratios=config.get('aspect_ratios', None), + angles=config.get('angles', None), + ) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the AnchorGenerator configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + + ''' + + if self.sizes: + cfg.MODEL.ANCHOR_GENERATOR.SIZES = self.sizes + if self.aspect_ratios: + cfg.MODEL.ANCHOR_GENERATOR.ASPECT_RATIOS = self.aspect_ratios + if self.angles: + cfg.MODEL.ANCHOR_GENERATOR.ANGLES = self.angles + +class RPNConfig: #pylint: disable=too-few-public-methods + '''RPNConfig + + Object representing RPN model configurations. + + Parameters + ---------- + iou_thresholds : list + Background and foreground thresholds for the IoU. + batch_size_per_image : int + Number of regions per image used to train the RPN. + ''' + def __init__( + self, + iou_thresholds : list = None, + batch_size_per_image : int = None, + ): + self.iou_thresholds = iou_thresholds + self.batch_size_per_image = batch_size_per_image + + @classmethod + def from_dictionary(cls, config: dict): + '''Build RPNConfig from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the RPNC configuration. + + Returns + ------- + RPNConfig + RPNConfig constructed with the parameters in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown parameter in config dict. + ''' + + available_configs = ['iou_thresholds', 'batch_size_per_image'] + for cfg_name in config.keys(): + if not cfg_name in available_configs: + err_msg = f'Unknown anchor generator parameter name "{cfg_name}". ' +\ + f'Available parameters are {available_configs}.' + raise InvalidModelConfigurationError(err_msg) + + default_config = { + } + + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + iou_thresholds=config.get('iou_thresholds', None), + batch_size_per_image=config.get('batch_size_per_image', None), + ) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the RPN configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + + ''' + + if self.iou_thresholds: + cfg.MODEL.RPN.IOU_THRESHOLDS = self.iou_thresholds + if self.batch_size_per_image: + cfg.MODEL.RPN.BATCH_SIZE_PER_IMAGE = self.batch_size_per_image + +class ROIHeadsConfig: #pylint: disable=too-few-public-methods + '''ROIHeadsConfig + + Object representing ROIHeads model configurations. + + Parameters + ---------- + iou_thresholds : List[float] + Overlap thresholds for an RoI to be considered foreground, by default None + batch_size_per_image : int, optional + Number of RoIs per image, by default None + ''' + + def __init__( + self, + iou_thresholds: List[float] = None, + batch_size_per_image: int = None, + ): + self.iou_thresholds = iou_thresholds + self.batch_size_per_image = batch_size_per_image + + @classmethod + def from_dictionary(cls, config: dict): + '''Build ROIHeadsConfig from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the ROIHeads configuration. + + Returns + ------- + ROIHeadsConfig + ROIHeadsConfig constructed with the parameters in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown parameter in config dict. + ''' + + available_configs = ['iou_thresholds', 'batch_size_per_image'] + for cfg_name in config.keys(): + if not cfg_name in available_configs: + err_msg = f'Unknown roi heads parameter name "{cfg_name}". ' +\ + f'Available parameters are {available_configs}.' + raise InvalidModelConfigurationError(err_msg) + + default_config = { + } + + if config.get('iou_thresholds', False): + if not isinstance(config['iou_thresholds'], list): + config['iou_thresholds'] = [config['iou_thresholds']] + + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + iou_thresholds=config.get('iou_thresholds', None), + batch_size_per_image=config.get('batch_size_per_image', None), + ) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the ROIHeads configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + + ''' + + if self.iou_thresholds: + cfg.MODEL.ROI_HEADS.IOU_THRESHOLDS = self.iou_thresholds + if self.batch_size_per_image: + cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE = self.batch_size_per_image + + +class BoxHeadConfig: #pylint: disable=too-few-public-methods + '''BoxHeadConfig + + Object representing BoxHead model configurations. + + Parameters + ---------- + class_agnostic : bool + Whether to apply class agnostic bbox regression. + train_on_pred_boxes : bool + Whether to use predicted BoxHead boxes instead of proposal boxes. + ''' + def __init__( + self, + class_agnostic : bool = None, + train_on_pred_boxes : bool = None, + ): + self.class_agnostic = class_agnostic + self.train_on_pred_boxes = train_on_pred_boxes + + @classmethod + def from_dictionary(cls, config: dict): + '''Build BoxHeadConfig from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the BoxHead configuration. + + Returns + ------- + BoxHeadConfig + BoxHeadConfig constructed with the parameters in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown parameter in config dict. + ''' + + available_configs = ['class_agnostic', 'train_on_pred_boxes'] + for cfg_name in config.keys(): + if not cfg_name in available_configs: + err_msg = f'Unknown anchor generator parameter name "{cfg_name}". ' +\ + f'Available parameters are {available_configs}.' + raise InvalidModelConfigurationError(err_msg) + + default_config = { + } + + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + class_agnostic=config.get('class_agnostic', None), + train_on_pred_boxes=config.get('train_on_pred_boxes', None), + ) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the BoxHead configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + + ''' + + if self.class_agnostic: + cfg.MODEL.ROI_BOX_HEAD.CLS_AGNOSTIC_BBOX_REG = self.class_agnostic + if self.train_on_pred_boxes: + cfg.MODEL.ROI_BOX_HEAD.TRAIN_ON_PRED_BOXES = self.train_on_pred_boxes + +class MaskHeadConfig: #pylint: disable=too-few-public-methods + '''MaskHeadConfig + + Object representing MaskHead model configurations. + + Parameters + ---------- + class_agnostic : bool + Whether to apply class agnostic mask regression. + pooler_resolution : int + Resolution of the pooling. + ''' + def __init__( + self, + class_agnostic : bool = None, + pooler_resolution : int = None, + ): + self.class_agnostic = class_agnostic + self.pooler_resolution = pooler_resolution + + @classmethod + def from_dictionary(cls, config: dict): + '''Build MaskHeadConfig from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the MaskHead configuration. + + Returns + ------- + MaskHeadConfig + MaskHeadConfig constructed with the parameters in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown parameter in config dict. + ''' + + available_configs = ['class_agnostic', 'pooler_resolution'] + for cfg_name in config.keys(): + if not cfg_name in available_configs: + err_msg = f'Unknown anchor generator parameter name "{cfg_name}". ' +\ + f'Available parameters are {available_configs}.' + raise InvalidModelConfigurationError(err_msg) + + default_config = { + } + + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + class_agnostic=config.get('class_agnostic', None), + pooler_resolution=config.get('pooler_resolution', None), + ) + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the BoxHead configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + + ''' + + if self.class_agnostic: + cfg.MODEL.ROI_MASK_HEAD.CLS_AGNOSTIC_MASK = self.class_agnostic + if self.pooler_resolution: + cfg.MODEL.ROI_MASK_HEAD.POOLER_RESOLUTION = self.pooler_resolution + +# see all models: detectron2.model_zoo.model_zoo._ModelZooUrls.CONFIG_PATH_TO_URL_SUFFIX +MODELS = { + 'faster_rcnn_R_50_C4_1x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_C4_1x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_50_DC5_1x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_DC5_1x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_50_FPN_1x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_FPN_1x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_50_C4_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_C4_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_50_DC5_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_DC5_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_50_FPN_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_50_FPN_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_101_C4_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_101_C4_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_101_DC5_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_101_DC5_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_R_101_FPN_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_R_101_FPN_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'faster_rcnn_X_101_32x8d_FPN_3x': { + 'config_file': 'COCO-Detection/faster_rcnn_X_101_32x8d_FPN_3x.yaml', + 'tasks': ['bbox-detection'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + }, + }, + 'mask_rcnn_R_50_C4_1x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_C4_1x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_50_DC5_1x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_DC5_1x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_50_FPN_1x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_50_C4_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_C4_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_50_DC5_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_DC5_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_50_FPN_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_101_C4_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_101_C4_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_101_DC5_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_101_DC5_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_R_101_FPN_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_R_101_FPN_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, + 'mask_rcnn_X_101_32x8d_FPN_3x': { + 'config_file': 'COCO-InstanceSegmentation/mask_rcnn_X_101_32x8d_FPN_3x.yaml', + 'tasks': ['instance-segmentation'], + 'model_parameters': { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'mask_head': MaskHeadConfig, + }, + }, +} + +MODEL_PARAMETERS_MAP = { + 'anchor_generator': AnchorGeneratorConfig, + 'rpn': RPNConfig, + 'roi_heads': ROIHeadsConfig, + 'box_head': BoxHeadConfig, + 'mask_head': MaskHeadConfig, +} + +class GinjinnModelConfiguration: #pylint: disable=too-few-public-methods + '''A class representing GinJinn model configurations. + + Parameters + ---------- + name : str + model name/identifier. + weights : str + Determines the initialization of the model weights. + One of + - '', meaning random weights initialization + - 'pretrained', meaning pretrained weights from the Detectron2 model zoo, if available + - the file path of a weights file. + classification_threshold: float + Classification threshold for training. + model_parameters: dict + dict of model-specific parameters. + + Raises + ------ + InvalidModelConfigurationError + Raised if invalid model name is passed. + ''' + def __init__( #pylint: disable=too-many-arguments,dangerous-default-value + self, + name: str, + weights: str, + classification_threshold: float, + model_parameters: dict = {}, + ): + self.name = name + if not name in MODELS.keys(): + raise InvalidModelConfigurationError('Invalid model name.') + + self.detectron2_config_name = MODELS[self.name]['config_file'] + + self.weights = weights + self.classification_threshold = classification_threshold + + self.model_parameters = model_parameters + + self._check_config() + + def to_detectron2_config(self): + '''to_detectron2_config + + Convert model configuration to Detectron2 configuration. + + Returns + ------- + detectron2_config + Detectron2 configuration. + ''' + + # import here to reduce loading times, when detectron2 conversion is not + # required + from detectron2.config import get_cfg #pylint: disable=import-outside-toplevel + from detectron2.model_zoo import get_config_file, get_checkpoint_url #pylint: disable=import-outside-toplevel + + cfg = get_cfg() + model_config_file = get_config_file(self.detectron2_config_name) + cfg.merge_from_file(model_config_file) + + if self.weights == 'pretrained': + model_url = get_checkpoint_url(self.detectron2_config_name) + cfg.MODEL.WEIGHTS = model_url + else: + cfg.MODEL.WEIGHTS = self.weights + + for model_param in self.model_parameters.values(): + model_param.update_detectron2_config(cfg) + + return cfg + + @classmethod + def from_dictionary(cls, config: dict): + '''Build GinjinnModelConfiguration from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the model configuration. + + Returns + ------- + GinjinnModelConfiguration + GinjinnModelConfiguration constructed with the configuration + given in config. + + Raises + ------ + InvalidModelConfigurationError + Raised if unknown model_parameters entry in config dict. + ''' + + default_config = { + 'weights': '', + 'classification_threshold': 0.5, + 'model_parameters': {}, + } + + # Maybe implement this more elegantly... + default_config.update(config) + config = copy.deepcopy(default_config) + + # model parameters + for mp_name, mp_cfg in config['model_parameters'].items(): + mp_class = MODEL_PARAMETERS_MAP.get(mp_name, None) + if not mp_class: + err_msg = f'Unknown model_parameters entry "{mp_name}". Available ' +\ + f'model_parameters are {", ".join(MODEL_PARAMETERS_MAP.keys())}.' + raise InvalidModelConfigurationError(err_msg) + + config['model_parameters'][mp_name] = mp_class.from_dictionary(mp_cfg) + + return cls( + name=config['name'], + weights=config['weights'], + classification_threshold=config['classification_threshold'], + model_parameters=config['model_parameters'], + ) + + def _check_weights(self): + '''Check weights option + + Raises + ------ + InvalidModelConfigurationError + Raised if an invalid weights option is passed. + ''' + + if self.weights != '' and self.weights != 'pretrained': + if not os.path.isfile(self.weights): + raise InvalidModelConfigurationError( + 'weights must be either "", "pretrained", or a valid weights file path.' #pylint: disable=line-too-long + ) + + def _check_classification_threshold(self): + '''_check_classification_threshold + + Raises + ------ + InvalidModelConfigurationError + Raised if an invalid classification_threshold option is passed. + ''' + if self.classification_threshold <= 0.0 or self.classification_threshold > 1.0: + raise InvalidModelConfigurationError( + 'classification_threshold must be between 0.0 and 1.0.' + ) + + def _check_model_parameters(self): + '''_check_model_parameters + + Raises + ------ + InvalidModelConfigurationError + Raised if an invalid model_parameters option is passed. + ''' + + for name, cfg in self.model_parameters.items(): + model_par_class = MODELS[self.name]['model_parameters'].get(name) + + if not model_par_class: + err_msg = f'Unknown model_parameters entry "{name}". Availabe model_parameters ' +\ + f'are {", ".join(MODELS[self.name]["model_parameters"].keys())}' + raise InvalidModelConfigurationError(err_msg) + + expected_class = MODELS[self.name]['model_parameters'][name] + if not isinstance(cfg, expected_class): + err_msg = f'Model parameter "{name}" must be instance of class {expected_class}.' + + def _check_config(self): + '''_check_config + + Check model configuration. + ''' + self._check_weights() + self._check_classification_threshold() + self._check_model_parameters() diff --git a/ginjinn/ginjinn_config/options_config.py b/ginjinn/ginjinn_config/options_config.py new file mode 100644 index 0000000..becabce --- /dev/null +++ b/ginjinn/ginjinn_config/options_config.py @@ -0,0 +1,122 @@ +''' +GinJinn options configuration module +''' + +import copy +import os +# from typing import Optional +from .config_error import InvalidOptionsConfigurationError + +N_CORES = os.cpu_count() + +class GinjinnOptionsConfiguration: #pylint: disable=too-few-public-methods + '''A class representing GinJinn model configurations. + + Parameters + ---------- + resume : bool + Determines, whether a previous run should be resumed + n_threads : int + Number of CPU threads to use. + device: str + Device to run the model on. E.g. "cuda", "cpu". + ''' + def __init__( #pylint: disable=too-many-arguments + self, + resume: bool, + n_threads: int, + device: str, + ): + self.resume = resume + self.n_threads = n_threads + self.device = device + + self._check_config() + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the options configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + ''' + + cfg.DATALOADER.NUM_WORKERS = self.n_threads + cfg.MODEL.DEVICE = self.device + + @classmethod + def from_dictionary(cls, config: dict): + '''Build GinjinnOptionsConfiguration from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the options configuration. + + Returns + ------- + GinjinnOptionsConfiguration + GinjinnOptionsConfiguration constructed with the configuration + given in config. + ''' + + default_config = { + 'resume': False, + 'n_threads': N_CORES - 1 if N_CORES > 1 else N_CORES, + 'device': 'cuda', + } + + # Maybe implement this more elegantly... + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + resume=config['resume'], + n_threads=config['n_threads'], + device=config['device'], + ) + + def _check_n_threads(self): + ''' Check n_threads config + + Raises + ------ + InvalidOptionsConfigurationError + Raised if n_threads value is invalid. + ''' + if self.n_threads < 1: + raise InvalidOptionsConfigurationError( + 'n_threads must be a positive number.' + ) + + def _check_device(self): + '''_check_device + + Raises + ------ + InvalidOptionsConfigurationError + Raised if device value is invalid. + ''' + err_msg = f'Invalid device option "{self.device}"; device should be either "cpu", ' +\ + '"cuda", or "cpu:" or "cuda:" followed by the respective device number (e.g "cuda:0").' + + if self.device in ['cpu', 'cuda']: + return + + if self.device.startswith('cpu:') or self.device.startswith('cuda:'): + try: + int(self.device.split(':')[1]) + except Exception as err: + raise InvalidOptionsConfigurationError(err_msg) from err + else: + raise InvalidOptionsConfigurationError(err_msg) + + + def _check_config(self): + ''' Check configuration values for validity. + ''' + self._check_n_threads() + self._check_device() diff --git a/ginjinn/ginjinn_config/tests/test_augmentation_configuration.py b/ginjinn/ginjinn_config/tests/test_augmentation_configuration.py new file mode 100644 index 0000000..d740f0d --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_augmentation_configuration.py @@ -0,0 +1,440 @@ +''' Module to test augmentation_config.py +''' + +import pytest + +from ginjinn.ginjinn_config import GinjinnAugmentationConfiguration, InvalidAugmentationConfigurationError +from ginjinn.ginjinn_config.augmentation_config import HorizontalFlipAugmentationConfiguration, \ + VerticalFlipAugmentationConfiguration, \ + RotationRangeAugmentationConfiguration, \ + RotationChoiceAugmentationConfiguration, \ + BrightnessAugmentationConfiguration, \ + ContrastAugmentationConfiguration, \ + SaturationAugmentationConfiguration, \ + CropRelativeAugmentationConfiguration, \ + CropAbsoluteAugmentationConfiguration + +@pytest.fixture +def simple_augmentation_list(): + return ( + [ + { + 'horizontal_flip': { + 'probability': 0.25 + } + }, + { + 'vertical_flip': { + 'probability': 0.25 + } + }, + { + 'rotation_range': { + 'angle_min': -10, + 'angle_max': 10, + 'expand': True, + 'probability': 0.25 + } + }, + { + 'rotation_choice': { + 'angles': [ + -10, + -20, + 10, + 20, + ], + 'expand': True, + 'probability': 0.25 + } + }, + { + 'brightness': { + 'brightness_min': 0.5, + 'brightness_max': 1.5, + 'probability': 0.75 + } + }, + { + 'contrast': { + 'contrast_min': 0.5, + 'contrast_max': 1.5, + 'probability': 0.75 + } + }, + { + 'saturation': { + 'saturation_min': 0.5, + 'saturation_max': 1.5, + 'probability': 0.75 + } + }, + { + 'crop_relative': { + 'width': 0.75, + 'height': 0.75, + 'probability': 0.3 + } + }, + { + 'crop_absolute': { + 'width': 128, + 'height': 128, + 'probability': 0.3 + } + }, + ], + [ + HorizontalFlipAugmentationConfiguration, + VerticalFlipAugmentationConfiguration, + RotationRangeAugmentationConfiguration, + RotationChoiceAugmentationConfiguration, + BrightnessAugmentationConfiguration, + ContrastAugmentationConfiguration, + SaturationAugmentationConfiguration, + CropRelativeAugmentationConfiguration, + CropAbsoluteAugmentationConfiguration, + ] + ) + +@pytest.fixture +def invalid_augmentation_list(): + return [ + { + 'invalid_augmentation': { + 'probability': 0.25 + } + }, + ] + +def test_simple(simple_augmentation_list): + aug = GinjinnAugmentationConfiguration.from_dictionaries( + simple_augmentation_list[0] + ) + + assert len(aug.augmentations) == len(simple_augmentation_list[0]) + # assert isinstance(aug.augmentations[0], simple_augmentation_list[1][0]) + assert aug.augmentations[0].probability == simple_augmentation_list[0][0]['horizontal_flip']['probability'] + # assert isinstance(aug.augmentations[1], simple_augmentation_list[1][1]) + assert aug.augmentations[1].probability == simple_augmentation_list[0][1]['vertical_flip']['probability'] + + for i, aug_conf in enumerate(aug.augmentations): + assert isinstance(aug_conf, simple_augmentation_list[1][i]) + +def test_invalid_aug_name(invalid_augmentation_list): + with pytest.raises(InvalidAugmentationConfigurationError): + aug = GinjinnAugmentationConfiguration.from_dictionaries(invalid_augmentation_list) + +def test_invalid_aug_class(): + with pytest.raises(InvalidAugmentationConfigurationError): + aug = GinjinnAugmentationConfiguration([{}, {}]) + +def test_empty_aug(): + aug_1 = GinjinnAugmentationConfiguration.from_dictionaries([]) + assert len(aug_1.augmentations) == 0 + + aug_2 = GinjinnAugmentationConfiguration([]) + assert len(aug_2.augmentations) == 0 + +def test_invalid_probability(): + with pytest.raises(InvalidAugmentationConfigurationError): + aug = GinjinnAugmentationConfiguration.from_dictionaries([ + {'horizontal_flip': { + 'probability': -0.1 + }} + ]) + + with pytest.raises(InvalidAugmentationConfigurationError): + aug = GinjinnAugmentationConfiguration.from_dictionaries([ + {'horizontal_flip': { + 'probability': 1.1 + }} + ]) + +def test_invalid_rotation_range(): + with pytest.raises(InvalidAugmentationConfigurationError): + RotationRangeAugmentationConfiguration.from_dictionary( + { + 'angle_min': 11, + 'angle_max': 10, + 'expand': True, + 'probability': 0.25 + } + ) + with pytest.raises(InvalidAugmentationConfigurationError): + RotationRangeAugmentationConfiguration.from_dictionary( + { + 'angle_min': -10, + 'expand': True, + 'probability': 0.25 + } + ) + with pytest.raises(InvalidAugmentationConfigurationError): + RotationRangeAugmentationConfiguration.from_dictionary( + { + 'angle_max': 20, + 'expand': True, + 'probability': 0.25 + } + ) + with pytest.raises(InvalidAugmentationConfigurationError): + RotationChoiceAugmentationConfiguration.from_dictionary( + { + 'angles': [], + 'expand': True, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + RotationChoiceAugmentationConfiguration.from_dictionary( + { + 'expand': True, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + BrightnessAugmentationConfiguration.from_dictionary( + { + 'brightness_min': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + BrightnessAugmentationConfiguration.from_dictionary( + { + 'brightness_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + BrightnessAugmentationConfiguration.from_dictionary( + { + 'brightness_min': 0.2, + 'brightness_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + BrightnessAugmentationConfiguration.from_dictionary( + { + 'brightness_min': -0.1, + 'brightness_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + BrightnessAugmentationConfiguration.from_dictionary( + { + 'brightness_min': 0.1, + 'brightness_max': -0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + ContrastAugmentationConfiguration.from_dictionary( + { + 'contrast_min': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + ContrastAugmentationConfiguration.from_dictionary( + { + 'contrast_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + ContrastAugmentationConfiguration.from_dictionary( + { + 'contrast_min': 0.2, + 'contrast_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + ContrastAugmentationConfiguration.from_dictionary( + { + 'contrast_min': -0.1, + 'contrast_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + ContrastAugmentationConfiguration.from_dictionary( + { + 'contrast_min': 0.1, + 'contrast_max': -0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + SaturationAugmentationConfiguration.from_dictionary( + { + 'saturation_min': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + SaturationAugmentationConfiguration.from_dictionary( + { + 'saturation_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + SaturationAugmentationConfiguration.from_dictionary( + { + 'saturation_min': 0.2, + 'saturation_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + SaturationAugmentationConfiguration.from_dictionary( + { + 'saturation_min': -0.1, + 'saturation_max': 0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + SaturationAugmentationConfiguration.from_dictionary( + { + 'saturation_min': 0.1, + 'saturation_max': -0.1, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + { + 'width': 0.7, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + { + 'height': 0.7, + 'probability': 0.25 + } + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + {'width': 0.7, 'height': 0.0, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + {'width': 0.7, 'height': 1.1, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + {'width': 0.0, 'height': 0.7, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + {'width': 1.1, 'height': 0.7, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropRelativeAugmentationConfiguration.from_dictionary( + {'width': 1.1, 'height': 1.1, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropAbsoluteAugmentationConfiguration.from_dictionary( + {'width': 128, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropAbsoluteAugmentationConfiguration.from_dictionary( + {'height': 128, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropAbsoluteAugmentationConfiguration.from_dictionary( + {'width': 128, 'height': 0, 'probability': 0.25} + ) + + with pytest.raises(InvalidAugmentationConfigurationError): + CropAbsoluteAugmentationConfiguration.from_dictionary( + {'width': 0, 'height': 128, 'probability': 0.25} + ) + + +def test_detectron2_conversion(simple_augmentation_list): + aug = GinjinnAugmentationConfiguration.from_dictionaries( + simple_augmentation_list[0] + ) + + d_augs = aug.to_detectron2_augmentations() + + assert d_augs[0].prob == simple_augmentation_list[0][0]['horizontal_flip']['probability'] + assert d_augs[0].horizontal == True + assert d_augs[0].vertical == False + + assert d_augs[1].prob == simple_augmentation_list[0][1]['vertical_flip']['probability'] + assert d_augs[1].horizontal == False + assert d_augs[1].vertical == True + + assert d_augs[2].prob == simple_augmentation_list[0][2]['rotation_range']['probability'] + assert d_augs[2].aug.angle[0] == simple_augmentation_list[0][2]['rotation_range']['angle_min'] + assert d_augs[2].aug.angle[1] == simple_augmentation_list[0][2]['rotation_range']['angle_max'] + assert d_augs[2].aug.expand == simple_augmentation_list[0][2]['rotation_range']['expand'] + + assert d_augs[3].prob == simple_augmentation_list[0][3]['rotation_choice']['probability'] + l1 = len(d_augs[3].aug.angle) + l2 = len(simple_augmentation_list[0][3]['rotation_choice']['angles']) + assert l1 == l2 + for a1, a2 in zip( + d_augs[3].aug.angle, + simple_augmentation_list[0][3]['rotation_choice']['angles'] + ): + assert a1 == a2 + + assert d_augs[3].aug.expand == simple_augmentation_list[0][3]['rotation_choice']['expand'] + + assert d_augs[4].prob == simple_augmentation_list[0][4]['brightness']['probability'] + assert d_augs[4].aug.intensity_min == simple_augmentation_list[0][4]['brightness']['brightness_min'] + assert d_augs[4].aug.intensity_max == simple_augmentation_list[0][4]['brightness']['brightness_max'] + + assert d_augs[5].prob == simple_augmentation_list[0][5]['contrast']['probability'] + assert d_augs[5].aug.intensity_min == simple_augmentation_list[0][5]['contrast']['contrast_min'] + assert d_augs[5].aug.intensity_max == simple_augmentation_list[0][5]['contrast']['contrast_max'] + + assert d_augs[6].prob == simple_augmentation_list[0][6]['saturation']['probability'] + assert d_augs[6].aug.intensity_min == simple_augmentation_list[0][6]['saturation']['saturation_min'] + assert d_augs[6].aug.intensity_max == simple_augmentation_list[0][6]['saturation']['saturation_max'] + + assert d_augs[7].prob == simple_augmentation_list[0][7]['crop_relative']['probability'] + assert d_augs[7].aug.crop_size[0] == simple_augmentation_list[0][7]['crop_relative']['height'] + assert d_augs[7].aug.crop_size[1] == simple_augmentation_list[0][7]['crop_relative']['width'] + assert d_augs[7].aug.crop_type == 'relative' + + assert d_augs[8].prob == simple_augmentation_list[0][8]['crop_absolute']['probability'] + assert d_augs[8].aug.crop_size[0] == simple_augmentation_list[0][8]['crop_absolute']['height'] + assert d_augs[8].aug.crop_size[1] == simple_augmentation_list[0][8]['crop_absolute']['width'] + assert d_augs[8].aug.crop_type == 'absolute' diff --git a/ginjinn/ginjinn_config/tests/test_ginjinn_config.py b/ginjinn/ginjinn_config/tests/test_ginjinn_config.py new file mode 100644 index 0000000..84d6294 --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_ginjinn_config.py @@ -0,0 +1,254 @@ +'''Tests for GinjinnConfiguration +''' + +import pkg_resources +import tempfile +import os +import pytest +import yaml +import copy + +from ginjinn.ginjinn_config import GinjinnConfiguration, InvalidGinjinnConfigurationError +from ginjinn.ginjinn_config.config_error import InvalidModelConfigurationError + +@pytest.fixture(scope='module', autouse=True) +def tmp_input_paths(): + tmpdir = tempfile.TemporaryDirectory() + + img_path_train = os.path.join(tmpdir.name, 'images_train') + os.mkdir(img_path_train) + img_path_test = os.path.join(tmpdir.name, 'images_test') + os.mkdir(img_path_test) + img_path_validation = os.path.join(tmpdir.name, 'images_validation') + os.mkdir(img_path_validation) + + pvoc_ann_path_train = os.path.join(tmpdir.name, 'annotations_train') + os.mkdir(pvoc_ann_path_train) + pvoc_ann_path_test = os.path.join(tmpdir.name, 'annotations_test') + os.mkdir(pvoc_ann_path_test) + pvoc_ann_path_validation = os.path.join(tmpdir.name, 'annotations_validation') + os.mkdir(pvoc_ann_path_validation) + + coco_ann_path_train = os.path.join(tmpdir.name, 'annotations_train.json') + with open(coco_ann_path_train, 'w') as ann_f: + ann_f.write('') + coco_ann_path_test = os.path.join(tmpdir.name, 'annotations_test.json') + with open(coco_ann_path_test, 'w') as ann_f: + ann_f.write('') + coco_ann_path_validation = os.path.join(tmpdir.name, 'annotations_valiation.json') + with open(coco_ann_path_validation, 'w') as ann_f: + ann_f.write('') + + yield { + 'coco_ann_path_train': coco_ann_path_train, + 'coco_ann_path_test': coco_ann_path_test, + 'coco_ann_path_validation': coco_ann_path_validation, + 'pvoc_ann_path_train': pvoc_ann_path_train, + 'pvoc_ann_path_test': pvoc_ann_path_test, + 'pvoc_ann_path_validation': pvoc_ann_path_validation, + 'img_path_train': img_path_train, + 'img_path_test': img_path_test, + 'img_path_validation': img_path_validation, + 'project_dir': tmpdir.name, + } + + tmpdir.cleanup() + + +@pytest.fixture +def config_dicts(tmp_input_paths): + simple_config = { + 'project_dir': tmp_input_paths['project_dir'], + 'task': 'bbox-detection', + 'input': { + 'type': 'PVOC', + 'training': { + 'annotation_path': tmp_input_paths['pvoc_ann_path_train'], + 'image_path': tmp_input_paths['img_path_train'], + }, + }, + 'model': { + 'name': 'faster_rcnn_R_50_FPN_3x', + 'weights': '', + 'model_parameters': { + 'roi_heads': { + 'batch_size_per_image': 4096, + 'iou_thresholds': [0.5], + }, + 'anchor_generator': { + 'sizes': [[32, 64, 128, 256]], + 'angles': [[-90, 0, 90]], + 'aspect_ratios': [[0.25, 0.5, 0.75, 1.0, 1.25]] + }, + 'rpn': { + 'iou_thresholds': [0.3, 0.7], + 'batch_size_per_image': 256, + }, + 'box_head': { + 'class_agnostic': False, + 'train_on_pred_boxes': False, + } + }, + }, + 'training': { + 'learning_rate': 0.002, + 'batch_size': 1, + 'max_iter': 10000, + }, + 'augmentation': [ + { + 'horizontal_flip': { + 'probability': 0.25 + } + }, + { + 'vertical_flip': { + 'probability': 0.25 + } + }, + ], + } + + return [ + simple_config + ] + +@pytest.fixture +def config_file_examples(): + example_config_0_path = pkg_resources.resource_filename( + 'ginjinn', 'data/ginjinn_config/example_config_0.yaml', + ) + example_config_1_path = pkg_resources.resource_filename( + 'ginjinn', 'data/ginjinn_config/example_config_1.yaml', + ) + + return [ + example_config_0_path, + example_config_1_path, + ] + +def read_config_file(file_path): + with open(file_path) as config_file: + config = yaml.safe_load(config_file) + return config + +def test_from_dictionary_simple(config_dicts): + simple_config_dict = config_dicts[0] + + ginjinn_config_0 = GinjinnConfiguration.from_dictionary(simple_config_dict) + assert ginjinn_config_0.task == simple_config_dict['task'] and\ + ginjinn_config_0.project_dir == simple_config_dict['project_dir'],\ + 'simple base configuration not set.' + # TODO implement model and augmentation assertions! + assert ginjinn_config_0.model.name == simple_config_dict['model']['name'] + assert ginjinn_config_0.training.learning_rate == simple_config_dict['training']['learning_rate'] + assert ginjinn_config_0.input.type == simple_config_dict['input']['type'] + +def test_from_config_file_simple(config_file_examples): + simple_config_file_0 = config_file_examples[0] + simple_config_dict_0 = read_config_file(simple_config_file_0) + print(simple_config_dict_0) + if not os.path.exists(simple_config_dict_0['input']['training']['annotation_path']): + os.mkdir(simple_config_dict_0['input']['training']['annotation_path']) + if not os.path.exists(simple_config_dict_0['input']['training']['image_path']): + os.mkdir(simple_config_dict_0['input']['training']['image_path']) + + simple_config_0 = GinjinnConfiguration.from_config_file(simple_config_file_0) + assert simple_config_0.task == simple_config_dict_0['task'] and\ + simple_config_0.project_dir == simple_config_dict_0['project_dir'] and\ + simple_config_0.input.train.annotation_path == os.path.abspath(simple_config_dict_0['input']['training']['annotation_path']) and\ + simple_config_0.input.train.image_path == os.path.abspath(simple_config_dict_0['input']['training']['image_path']),\ + 'GinjinnConfig was not successfully constructed from simple configuration file.' + + assert simple_config_0.model.name == simple_config_dict_0['model']['name'] + assert simple_config_0.training.learning_rate == simple_config_dict_0['training']['learning_rate'] + + os.rmdir(simple_config_dict_0['input']['training']['annotation_path']) + os.rmdir(simple_config_dict_0['input']['training']['image_path']) + + + simple_config_file_1 = config_file_examples[1] + simple_config_dict_1 = read_config_file(simple_config_file_1) + print(simple_config_dict_1) + if not os.path.exists(simple_config_dict_1['input']['training']['annotation_path']): + with open(simple_config_dict_1['input']['training']['annotation_path'], 'w') as f: + f.write('') + if not os.path.exists(simple_config_dict_1['input']['training']['image_path']): + os.mkdir(simple_config_dict_1['input']['training']['image_path']) + + simple_config_1 = GinjinnConfiguration.from_config_file(simple_config_file_1) + assert simple_config_1.task == simple_config_dict_1['task'] and\ + simple_config_1.project_dir == simple_config_dict_1['project_dir'] and\ + simple_config_1.input.train.annotation_path == os.path.abspath(simple_config_dict_1['input']['training']['annotation_path']) and\ + simple_config_1.input.train.image_path == os.path.abspath(simple_config_dict_1['input']['training']['image_path']),\ + 'GinjinnConfig was not successfully constructed from simple configuration file.' + + assert simple_config_1.model.name == simple_config_dict_1['model']['name'] + assert simple_config_1.training.learning_rate == simple_config_dict_1['training']['learning_rate'] + + os.remove(simple_config_dict_1['input']['training']['annotation_path']) + os.rmdir(simple_config_dict_1['input']['training']['image_path']) + + +def test_invalid_task(config_dicts): + simple_config_dict = copy.deepcopy(config_dicts[0]) + simple_config_dict['task'] = 'foobar' + + with pytest.raises(InvalidGinjinnConfigurationError): + GinjinnConfiguration.from_dictionary(simple_config_dict) + +def test_incompatible_task(config_dicts): + simple_config_dict = copy.deepcopy(config_dicts[0]) + simple_config_dict['task'] = 'instance-segmentation' + + with pytest.raises(InvalidGinjinnConfigurationError): + GinjinnConfiguration.from_dictionary(simple_config_dict) + +def test_to_detectron2_config(config_dicts): + simple_config_dict = config_dicts[0] + + ginjinn_config_0 = GinjinnConfiguration.from_dictionary(simple_config_dict) + detectron2_config_0 = ginjinn_config_0.to_detectron2_config() + + # TODO additional tests + +def test_model_parameters(config_dicts): + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['model_parameters']['invalid_parameter'] = {} + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['model_parameters']['anchor_generator']['invalid_parameter'] = 1 + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['model_parameters']['rpn']['invalid_parameter'] = 1 + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['model_parameters']['roi_heads']['invalid_parameter'] = 1 + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['model_parameters']['box_head']['invalid_parameter'] = 1 + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict = copy.deepcopy(config_dicts[0]) + config_dict['model']['name'] = 'mask_rcnn_R_50_C4_1x' + config_dict['task'] = 'instance-segmentation' + del config_dict['model']['model_parameters']['box_head'] + config_dict['model']['model_parameters']['mask_head'] = { + 'class_agnostic': False, + 'pooler_resolution': 10, + } + GinjinnConfiguration.from_dictionary(config_dict) + + config_dict['model']['model_parameters']['mask_head']['invalid_parameter'] = 1 + with pytest.raises(InvalidModelConfigurationError): + GinjinnConfiguration.from_dictionary(config_dict) diff --git a/ginjinn/ginjinn_config/tests/test_input_configuration.py b/ginjinn/ginjinn_config/tests/test_input_configuration.py new file mode 100644 index 0000000..2c2f291 --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_input_configuration.py @@ -0,0 +1,343 @@ +'''Tests for GinjinnInputconfiguration +''' + +import pytest +import tempfile +import os +import copy +from ginjinn.ginjinn_config import GinjinnInputConfiguration, InvalidInputConfigurationError +from ginjinn.ginjinn_config.input_config import SplitConfig + +@pytest.fixture(scope='module', autouse=True) +def tmp_input_paths(): + tmpdir = tempfile.TemporaryDirectory() + + img_path_train = os.path.join(tmpdir.name, 'images_train') + os.mkdir(img_path_train) + img_path_test = os.path.join(tmpdir.name, 'images_test') + os.mkdir(img_path_test) + img_path_validation = os.path.join(tmpdir.name, 'images_validation') + os.mkdir(img_path_validation) + + pvoc_ann_path_train = os.path.join(tmpdir.name, 'annotations_train') + os.mkdir(pvoc_ann_path_train) + pvoc_ann_path_test = os.path.join(tmpdir.name, 'annotations_test') + os.mkdir(pvoc_ann_path_test) + pvoc_ann_path_validation = os.path.join(tmpdir.name, 'annotations_validation') + os.mkdir(pvoc_ann_path_validation) + + coco_ann_path_train = os.path.join(tmpdir.name, 'annotations_train.json') + with open(coco_ann_path_train, 'w') as ann_f: + ann_f.write('') + coco_ann_path_test = os.path.join(tmpdir.name, 'annotations_test.json') + with open(coco_ann_path_test, 'w') as ann_f: + ann_f.write('') + coco_ann_path_validation = os.path.join(tmpdir.name, 'annotations_valiation.json') + with open(coco_ann_path_validation, 'w') as ann_f: + ann_f.write('') + + yield { + 'coco_ann_path_train': coco_ann_path_train, + 'coco_ann_path_test': coco_ann_path_test, + 'coco_ann_path_validation': coco_ann_path_validation, + 'pvoc_ann_path_train': pvoc_ann_path_train, + 'pvoc_ann_path_test': pvoc_ann_path_test, + 'pvoc_ann_path_validation': pvoc_ann_path_validation, + 'img_path_train': img_path_train, + 'img_path_test': img_path_test, + 'img_path_validation': img_path_validation, + } + + tmpdir.cleanup() + + +@pytest.fixture +def basic_inputs(tmp_input_paths): + return [ + 'PVOC', + tmp_input_paths['pvoc_ann_path_train'], + tmp_input_paths['img_path_train'] + ] + +@pytest.fixture +def custom_split_inputs_pvoc(tmp_input_paths): + return [ + tmp_input_paths['pvoc_ann_path_test'], + tmp_input_paths['img_path_test'], + tmp_input_paths['pvoc_ann_path_validation'], + tmp_input_paths['img_path_validation'], + ] + +@pytest.fixture +def automatic_split_inputs(): + return [ + 0.2, + 0.2 + ] + +@pytest.fixture +def config_dicts(tmp_input_paths): + simple_config = { + 'type': 'PVOC', + 'training': { + 'annotation_path': tmp_input_paths['pvoc_ann_path_train'], + 'image_path': tmp_input_paths['img_path_train'], + }, + } + test_custom_0 = { + 'test': { + 'annotation_path': tmp_input_paths['pvoc_ann_path_test'], + 'image_path': tmp_input_paths['img_path_test'], + } + } + val_custom_0 = { + 'validation': { + 'annotation_path': tmp_input_paths['pvoc_ann_path_validation'], + 'image_path': tmp_input_paths['img_path_validation'], + } + } + + test_custom_config_0 = copy.deepcopy(simple_config) + test_custom_config_0.update(test_custom_0) + + val_custom_config_0 = copy.deepcopy(simple_config) + val_custom_config_0.update(val_custom_0) + + test_val_custom_config_0 = copy.deepcopy(simple_config) + test_val_custom_config_0.update(test_custom_0) + test_val_custom_config_0.update(val_custom_0) + + return [ + simple_config, + test_custom_config_0, + val_custom_config_0, + test_val_custom_config_0 + ] + +def test_constructor_simple(basic_inputs): + '''Simple constructor test. + ''' + + ann_type = basic_inputs[0] + train_ann_path = basic_inputs[1] + train_img_path = basic_inputs[2] + + input_configuration = GinjinnInputConfiguration( + ann_type, + train_ann_path, + train_img_path, + ) + + assert input_configuration.type == ann_type,\ + 'annotation type not set correctly' + assert input_configuration.train.annotation_path == train_ann_path,\ + 'train annotation path not set correctly' + assert input_configuration.train.image_path == train_img_path,\ + 'train image path not set correctly' + +def test_constructor_custom_split(basic_inputs, custom_split_inputs_pvoc): + '''Test constructor with custom split. + ''' + + ann_type = basic_inputs[0] + train_ann_path = basic_inputs[1] + train_img_path = basic_inputs[2] + + test_ann_path = custom_split_inputs_pvoc[0] + test_img_path = custom_split_inputs_pvoc[1] + val_ann_path = custom_split_inputs_pvoc[2] + val_img_path = custom_split_inputs_pvoc[3] + + # test split + input_configuration_0 = GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + test_ann_path=test_ann_path, + test_img_path=test_img_path + ) + assert input_configuration_0.test.annotation_path == test_ann_path,\ + 'test annotation path not set correctly' + assert input_configuration_0.test.image_path == test_img_path,\ + 'test image path not set correctly' + + + # validation split + input_configuration_1 = GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + val_ann_path=val_ann_path, + val_img_path=val_img_path + ) + assert input_configuration_1.val.annotation_path == val_ann_path,\ + 'validation annotation path not set correctly' + assert input_configuration_1.val.image_path == val_img_path,\ + 'validation image path not set correctly' + + # test-validation split + input_configuration_2 = GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + test_ann_path=test_ann_path, + test_img_path=test_img_path, + val_ann_path=val_ann_path, + val_img_path=val_img_path + ) + + assert input_configuration_2.test.annotation_path == test_ann_path,\ + 'test annotation path not set correctly (test-val)' + assert input_configuration_2.test.image_path == test_img_path,\ + 'test image path not set correctly (test-val)' + assert input_configuration_2.val.annotation_path == val_ann_path,\ + 'validation annotation path not set correctly (test-val)' + assert input_configuration_2.val.image_path == val_img_path,\ + 'validation image path not set correctly (test-val)' + +def test_invalid_annotation_type(basic_inputs): + ann_type = 'CVAT' + train_ann_path = basic_inputs[1] + train_img_path = basic_inputs[2] + + with pytest.raises(InvalidInputConfigurationError): + input_configuration = GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path + ) + +def test_missing_test_val_paths(basic_inputs, custom_split_inputs_pvoc): + ann_type = basic_inputs[0] + train_ann_path = basic_inputs[1] + train_img_path = basic_inputs[2] + + test_ann_path = custom_split_inputs_pvoc[0] + test_img_path = custom_split_inputs_pvoc[1] + val_ann_path = custom_split_inputs_pvoc[2] + val_img_path = custom_split_inputs_pvoc[3] + + with pytest.raises(InvalidInputConfigurationError): + GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + test_ann_path=test_ann_path, + ) + + with pytest.raises(InvalidInputConfigurationError): + GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + test_img_path=test_img_path, + ) + + with pytest.raises(InvalidInputConfigurationError): + GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + val_ann_path=val_ann_path, + ) + + with pytest.raises(InvalidInputConfigurationError): + GinjinnInputConfiguration( + ann_type, train_ann_path, train_img_path, + val_img_path=val_img_path, + ) + +def test_from_dictionary(config_dicts, tmp_input_paths): + simple_config = config_dicts[0] + test_custom_config_0 = config_dicts[1] + val_custom_config_0 = config_dicts[2] + test_val_custom_config_0 = config_dicts[3] + + input_configuration_0 = GinjinnInputConfiguration.from_dictionary(simple_config) + assert input_configuration_0.type == simple_config['type'] and\ + input_configuration_0.train.annotation_path == simple_config['training']['annotation_path'] and\ + input_configuration_0.train.image_path == simple_config['training']['image_path'],\ + 'Simple configuration from dictionary not successful.' + + input_configuration_1 = GinjinnInputConfiguration.from_dictionary(test_custom_config_0) + assert input_configuration_1.type == test_custom_config_0['type'] and\ + input_configuration_1.train.annotation_path == test_custom_config_0['training']['annotation_path'] and\ + input_configuration_1.train.image_path == test_custom_config_0['training']['image_path'] and\ + input_configuration_1.test.annotation_path == test_custom_config_0['test']['annotation_path'] and\ + input_configuration_1.test.image_path == test_custom_config_0['test']['image_path'],\ + 'Custom test configuration from dictionary not successful.' + + input_configuration_2 = GinjinnInputConfiguration.from_dictionary(val_custom_config_0) + assert input_configuration_2.type == test_custom_config_0['type'] and\ + input_configuration_2.train.annotation_path == val_custom_config_0['training']['annotation_path'] and\ + input_configuration_2.train.image_path == val_custom_config_0['training']['image_path'] and\ + input_configuration_2.val.annotation_path == val_custom_config_0['validation']['annotation_path'] and\ + input_configuration_2.val.image_path == val_custom_config_0['validation']['image_path'],\ + 'Custom validation configuration from dictionary not successful.' + + input_configuration_3 = GinjinnInputConfiguration.from_dictionary(test_val_custom_config_0) + assert input_configuration_3.type == test_val_custom_config_0['type'] and\ + input_configuration_3.train.annotation_path == test_val_custom_config_0['training']['annotation_path'] and\ + input_configuration_3.train.image_path == test_val_custom_config_0['training']['image_path'] and\ + input_configuration_3.val.annotation_path == test_val_custom_config_0['validation']['annotation_path'] and\ + input_configuration_3.val.image_path == test_val_custom_config_0['validation']['image_path'] and\ + input_configuration_3.test.annotation_path == test_val_custom_config_0['test']['annotation_path'] and\ + input_configuration_3.test.image_path == test_val_custom_config_0['test']['image_path'],\ + 'Custom test-validation configuration from dictionary not successful.' + + + input_configuration_5 = GinjinnInputConfiguration.from_dictionary({ + 'type': 'COCO', + 'training': { + 'annotation_path': tmp_input_paths['coco_ann_path_train'], + 'image_path': tmp_input_paths['img_path_train'] + }, + 'test': { + 'annotation_path': tmp_input_paths['coco_ann_path_test'], + 'image_path': tmp_input_paths['img_path_test'] + }, + 'validation': { + 'annotation_path': tmp_input_paths['coco_ann_path_validation'], + 'image_path': tmp_input_paths['img_path_validation'] + }, + }) + +def test_invalid_paths(tmp_input_paths): + with pytest.raises(InvalidInputConfigurationError): + input_dict = { + 'type': 'PVOC', + 'training': { + 'annotation_path': tmp_input_paths['coco_ann_path_train'], + 'image_path': tmp_input_paths['img_path_train'] + } + } + + GinjinnInputConfiguration.from_dictionary(input_dict) + + with pytest.raises(InvalidInputConfigurationError): + input_dict = { + 'type': 'COCO', + 'training': { + 'annotation_path': tmp_input_paths['pvoc_ann_path_train'], + 'image_path': tmp_input_paths['img_path_train'] + } + } + + GinjinnInputConfiguration.from_dictionary(input_dict) + + with pytest.raises(InvalidInputConfigurationError): + input_dict = { + 'type': 'COCO', + 'training': { + 'annotation_path': tmp_input_paths['coco_ann_path_train'], + + # just using this here to have file instead of a directory + 'image_path': tmp_input_paths['coco_ann_path_train'] + } + } + + GinjinnInputConfiguration.from_dictionary(input_dict) + +def test_SplitConfig(): + sc_0 = SplitConfig(0.2, 0.3) + assert sc_0.test == 0.2 + assert sc_0.val == 0.3 + + with pytest.raises(InvalidInputConfigurationError): + SplitConfig(0.0, 0.3) + with pytest.raises(InvalidInputConfigurationError): + SplitConfig(1.0, 0.3) + + with pytest.raises(InvalidInputConfigurationError): + SplitConfig(0.2, 0.0) + with pytest.raises(InvalidInputConfigurationError): + SplitConfig(0.2, 1.0) + + with pytest.raises(InvalidInputConfigurationError): + SplitConfig(0.5, 0.5) \ No newline at end of file diff --git a/ginjinn/ginjinn_config/tests/test_model_configuration.py b/ginjinn/ginjinn_config/tests/test_model_configuration.py new file mode 100644 index 0000000..ebdcbdd --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_model_configuration.py @@ -0,0 +1,65 @@ +'''GinjinnModelConfiguration test module +''' + +import pytest +import tempfile +import os +from ginjinn.ginjinn_config.model_config import GinjinnModelConfiguration, MODELS +from ginjinn.ginjinn_config.config_error import InvalidModelConfigurationError + +@pytest.fixture(scope='module', autouse=True) +def tmp_input_path(): + tmpdir = tempfile.TemporaryDirectory() + + tmp_file_path = os.path.join(tmpdir.name, 'weights.pkl') + + with open(tmp_file_path, 'w') as tmp_f: + tmp_f.write('') + + yield tmp_file_path + + tmpdir.cleanup() + +def test_simple_model(tmp_input_path): + name = list(MODELS.keys())[0] + weights = '' + + model = GinjinnModelConfiguration( + name=name, + weights=weights, + classification_threshold=0.5, + ) + + assert model.name == name + assert model.weights == weights + + weights = 'pretrained' + model = GinjinnModelConfiguration( + name=name, + weights=weights, + classification_threshold=0.5, + ) + + assert model.name == name + assert model.weights == weights + +def test_invalid_model(): + name = 'some_invalid_name' + weights = '' + + with pytest.raises(InvalidModelConfigurationError): + model = GinjinnModelConfiguration( + name=name, + weights = '', + classification_threshold=0.5, + ) + + valid_name = list(MODELS.keys())[0] + with pytest.raises(InvalidModelConfigurationError): + GinjinnModelConfiguration(name=valid_name, weights='xyz', classification_threshold=0.5,) + + with pytest.raises(InvalidModelConfigurationError): + GinjinnModelConfiguration(name=valid_name, weights='', classification_threshold=-0.1,) + + with pytest.raises(InvalidModelConfigurationError): + GinjinnModelConfiguration(name=valid_name, weights='', classification_threshold=1.1,) diff --git a/ginjinn/ginjinn_config/tests/test_options_config.py b/ginjinn/ginjinn_config/tests/test_options_config.py new file mode 100644 index 0000000..5ecb3cc --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_options_config.py @@ -0,0 +1,75 @@ +''' Module for testing options_config.py +''' + +import pytest +from ginjinn.ginjinn_config.options_config import GinjinnOptionsConfiguration +from ginjinn.ginjinn_config.config_error import InvalidOptionsConfigurationError + +def test_simple(): + n_threads=1 + resume=True + device='cuda' + + options_0 = GinjinnOptionsConfiguration( + resume=resume, + n_threads=n_threads, + device=device, + ) + assert options_0.resume == resume + assert options_0.n_threads == n_threads + assert options_0.device == device + + options_1 = GinjinnOptionsConfiguration.from_dictionary({ + 'n_threads': n_threads, + 'resume': resume, + }) + assert options_1.resume == resume + assert options_1.n_threads == n_threads + assert options_1.device == device + + options_2 = GinjinnOptionsConfiguration( + resume=resume, + n_threads=n_threads, + device='cpu', + ) + assert options_2.resume == resume + assert options_2.n_threads == n_threads + assert options_2.device == 'cpu' + + GinjinnOptionsConfiguration( + resume=resume, + n_threads=n_threads, + device='cpu:0', + ) + + GinjinnOptionsConfiguration( + resume=resume, + n_threads=n_threads, + device='cuda:0', + ) + +def test_invalid(): + n_threads=0 + resume=True + device='cuda' + + with pytest.raises(InvalidOptionsConfigurationError): + GinjinnOptionsConfiguration( + resume=resume, + n_threads=n_threads, + device=device, + ) + + with pytest.raises(InvalidOptionsConfigurationError): + GinjinnOptionsConfiguration( + resume=resume, + n_threads=2, + device='', + ) + + with pytest.raises(InvalidOptionsConfigurationError): + GinjinnOptionsConfiguration( + resume=resume, + n_threads=2, + device='cuda:x', + ) diff --git a/ginjinn/ginjinn_config/tests/test_read_example_config.py b/ginjinn/ginjinn_config/tests/test_read_example_config.py new file mode 100644 index 0000000..9602a46 --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_read_example_config.py @@ -0,0 +1,14 @@ +import pkg_resources +import yaml + +def test_yaml_config_loading(): + '''test_yaml_config_loading + + Summary + ------- + Test to check, whether the example config can be loaded using + the yaml python package. + ''' + example_config_0_path = pkg_resources.resource_filename('ginjinn', 'data/ginjinn_config/example_config_0.yaml') + with open(example_config_0_path) as f: + example_config_0 = yaml.safe_load(f) diff --git a/ginjinn/ginjinn_config/tests/test_train_config.py b/ginjinn/ginjinn_config/tests/test_train_config.py new file mode 100644 index 0000000..7424b23 --- /dev/null +++ b/ginjinn/ginjinn_config/tests/test_train_config.py @@ -0,0 +1,77 @@ +'''GinjinnTrainingConfiguration test module +''' + +import pytest +from ginjinn.ginjinn_config.training_config import GinjinnTrainingConfiguration +from ginjinn.ginjinn_config.config_error import InvalidTrainingConfigurationError + +@pytest.fixture +def simple_config(): + return { + "learning_rate": 0.002, + "batch_size": 1, + "max_iter": 40000, + "eval_period": 4000, + } + +def test_simple_training_config(simple_config): + learning_rate = simple_config['learning_rate'] + batch_size = simple_config['batch_size'] + max_iter = simple_config['max_iter'] + eval_period = simple_config['eval_period'] + + training = GinjinnTrainingConfiguration( + learning_rate = learning_rate, + batch_size = batch_size, + max_iter = max_iter, + eval_period = eval_period, + ) + + assert training.learning_rate == learning_rate + assert training.batch_size == batch_size + assert training.max_iter == max_iter + assert training.eval_period == eval_period + +def test_invalid_training_config(simple_config): + learning_rate = simple_config['learning_rate'] + batch_size = simple_config['batch_size'] + max_iter = simple_config['max_iter'] + + with pytest.raises(InvalidTrainingConfigurationError): + training = GinjinnTrainingConfiguration( + learning_rate = -1, + batch_size = batch_size, + max_iter = max_iter, + ) + + with pytest.raises(InvalidTrainingConfigurationError): + training = GinjinnTrainingConfiguration( + learning_rate = learning_rate, + batch_size = 0, + max_iter = max_iter, + ) + + with pytest.raises(InvalidTrainingConfigurationError): + training = GinjinnTrainingConfiguration( + learning_rate = learning_rate, + batch_size = batch_size, + max_iter = -1, + ) + + with pytest.raises(InvalidTrainingConfigurationError): + training = GinjinnTrainingConfiguration( + learning_rate = learning_rate, + batch_size = batch_size, + max_iter = max_iter, + eval_period = -1, + ) + +def test_from_dictionary(simple_config): + training = GinjinnTrainingConfiguration.from_dictionary( + simple_config + ) + + assert training.learning_rate == simple_config['learning_rate'] + assert training.batch_size == simple_config['batch_size'] + assert training.max_iter == simple_config['max_iter'] + assert training.eval_period == simple_config['eval_period'] diff --git a/ginjinn/ginjinn_config/training_config.py b/ginjinn/ginjinn_config/training_config.py new file mode 100644 index 0000000..62f1e4b --- /dev/null +++ b/ginjinn/ginjinn_config/training_config.py @@ -0,0 +1,210 @@ +''' +GinJinn training configuration module +''' + +import copy +# from typing import Optional +from .config_error import InvalidTrainingConfigurationError + +class GinjinnTrainingConfiguration: #pylint: disable=too-few-public-methods + '''A class representing GinJinn training configurations. + + Parameters + ---------- + learning_rate : float + learning rate for model training. + batch_size : int + batch size for model training and evaluation. + max_iter: int + maximum number of training iterations. + warmup_iter: int + number of warmup iterations. + momentum: float + momentum for solver. + eval_period: int + evaluation period. + checkpoint_period: int + checkpoint period. + ''' + def __init__( #pylint: disable=too-many-arguments + self, + learning_rate: float, + batch_size: int, + max_iter: int, + warmup_iter: int = 1000, + momentum: float = 0.9, + eval_period: int = 0, + checkpoint_period: int = 0, + ): + self.learning_rate = learning_rate + self.batch_size = batch_size + self.max_iter = max_iter + self.warmup_iter = warmup_iter + self.momentum = momentum + self.eval_period = eval_period + self.checkpoint_period = checkpoint_period + + self._check_config() + + def update_detectron2_config(self, cfg): + '''update_detectron2_config + + Updates detectron2 config with the training configuration. + + Parameters + ---------- + cfg + Detectron2 configuration + ''' + + cfg.SOLVER.IMS_PER_BATCH = self.batch_size + cfg.SOLVER.BASE_LR = self.learning_rate + cfg.SOLVER.MAX_ITER = self.max_iter + cfg.SOLVER.WARMUP_ITERS = self.warmup_iter + cfg.SOLVER.MOMENTUM = self.momentum + cfg.TEST.EVAL_PERIOD = self.eval_period + cfg.SOLVER.CHECKPOINT_PERIOD = self.checkpoint_period + + @classmethod + def from_dictionary(cls, config: dict): + '''Build GinjinnTrainingConfiguration from a dictionary object. + + Parameters + ---------- + config : dict + Dictionary object containing the training configuration. + + Returns + ------- + GinjinnTrainingConfiguration + GinjinnTrainingConfiguration constructed with the configuration + given in config. + ''' + + default_config = { + 'learning_rate': 0.001, + 'batch_size': 1, + 'max_iter': 40000, + 'warmup_iter': 1000, + 'momentum': 0.9, + 'eval_period': 0, + 'checkpoint_period': 5000, + } + + # Maybe implement this more elegantly... + default_config.update(config) + config = copy.deepcopy(default_config) + + return cls( + learning_rate=config['learning_rate'], + batch_size=config['batch_size'], + max_iter=config['max_iter'], + warmup_iter=config['warmup_iter'], + momentum=config['momentum'], + eval_period=config['eval_period'], + checkpoint_period=config['checkpoint_period'], + ) + + def _check_learning_rate(self): + ''' Check learning rate config + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid learning rate values. + ''' + if self.learning_rate < 0.0: + raise InvalidTrainingConfigurationError( + 'learning_rate must be greater than 0' + ) + + def _check_batch_size(self): + ''' Check batch size config + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid batch size values. + ''' + if self.batch_size < 1: + raise InvalidTrainingConfigurationError( + 'batch_size must be greater than or equal to 1' + ) + + def _check_max_iter(self): + ''' Check max iter config + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid max iter values. + ''' + if self.max_iter < 1: + raise InvalidTrainingConfigurationError( + 'max_iter must be greater than or equal to 1' + ) + + def _check_warmup_iter(self): + ''' Check warmup iter config + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid warmup iter values. + ''' + if self.warmup_iter < 1: + raise InvalidTrainingConfigurationError( + 'warmup_iter must be greater than or equal to 1' + ) + + def _check_momentum(self): + '''_check_momentum + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid momentum value. + ''' + + if self.momentum < 0.0: + raise InvalidTrainingConfigurationError( + 'momentum must be positive.' + ) + + def _check_eval_period(self): + '''_check_eval_period + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid eval_period value. + ''' + + if self.eval_period < 0: + raise InvalidTrainingConfigurationError( + 'eval_period must be positive.' + ) + + def _check_checkpoint_period(self): + '''_check_checkpoint_period + + Raises + ------ + InvalidTrainingConfigurationError + Raised for invalid checkpoint_period value. + ''' + + if self.eval_period < 0: + raise InvalidTrainingConfigurationError( + 'checkpoint_period must be positive.' + ) + + def _check_config(self): + ''' Check configs + ''' + self._check_learning_rate() + self._check_batch_size() + self._check_max_iter() + self._check_momentum() + self._check_eval_period() + self._check_checkpoint_period() diff --git a/ginjinn/predictor/__init__.py b/ginjinn/predictor/__init__.py new file mode 100644 index 0000000..49b40e9 --- /dev/null +++ b/ginjinn/predictor/__init__.py @@ -0,0 +1,4 @@ +''' Predictor module +''' + +from .predictors import GinjinnPredictor diff --git a/ginjinn/predictor/predictors.py b/ginjinn/predictor/predictors.py new file mode 100644 index 0000000..c6d7f3f --- /dev/null +++ b/ginjinn/predictor/predictors.py @@ -0,0 +1,480 @@ +""" +Classes for prediction, output formatting, and visualization. +""" + +import datetime +import glob +import json +import os +import pickle +from tempfile import NamedTemporaryFile +from typing import Iterable, List, Optional, Union +import numpy as np +import cv2 +import imantics +from detectron2.config import CfgNode +from detectron2.data import MetadataCatalog +from detectron2.engine.defaults import DefaultPredictor +from detectron2.utils.visualizer import _create_text_labels, ColorMode, GenericMask +from detectron2.utils.visualizer import VisImage, Visualizer +import ginjinn.segmentation_refinement as refine +import torch +from ginjinn.data_reader.data_reader import get_class_names +from ginjinn.ginjinn_config import GinjinnConfiguration +from ginjinn.utils.utils import bbox_from_polygons + + +class GinjinnPredictor(): + '''A class for predicting from a trained Detectron2 model. + + Parameters + ---------- + cfg : CfgNode + Detectron2 configuration object + class_names : list of str + Ordered list of object class names + img_dir : str + Directory containing input images for inference + outdir : str + Directory for writing results + task : str + "bbox-detection" or "instance-segmentation" + ''' + def __init__( + self, + cfg: CfgNode, + class_names: List[str], + img_dir: str, + outdir: str, + task: str + ): + self.class_names = class_names + self.d2_cfg = cfg + self.img_dir = img_dir + self.outdir = outdir + self.task = task + self._coco_annotations = dict() + self._coco_images = dict() + + @classmethod + def from_ginjinn_config( + cls, + gj_cfg: GinjinnConfiguration, + img_dir: str, + outdir: str, + checkpoint_name: str = "model_final.pth", + ) -> "GinjinnPredictor": + """ + Build GinjinnPredictor object from GinjinnConfiguration instead of + Detectron2 configuration. + + Parameters + ---------- + gj_cfg : GinjinnConfiguration + img_dir : str + Directory containing input images for inference + outdir : str + Directory for writing results + checkpoint_name : str + Name of the checkpoint to use. + + Returns + ------- + GinjinnPredictor + """ + + d2_cfg = gj_cfg.to_detectron2_config() + d2_cfg.MODEL.WEIGHTS = os.path.join(d2_cfg.OUTPUT_DIR, checkpoint_name) + + return cls( + d2_cfg, + get_class_names(gj_cfg.project_dir), + img_dir, + outdir, + gj_cfg.task + ) + + + def predict( + self, + img_names: List[str] = [], + output_options: Iterable[str] = ("COCO", "cropped", "visualization"), + padding: int = 0, + threshold: Union[float, int] = 0.8, + seg_refinement: bool = False, + refinement_device: str = "cuda:0", + refinement_method: str = "full" + ): + """ + img_names : list of str, default=[] + File names of images to be used as input. By default, all images within self.img_dir + will be used. + output_options : iterable of str, default=("COCO", "cropped", "visualization") + Available output formats: + "COCO": Write predictions to COCO json file. For better compatibility with external + programs, the annotations do not contain polygons consisting of less than + three points (i.e., tiny sub-objects consisting of only one or two pixels). + "cropped": Save cropped images and segmentation masks (if available). + In case of instance segmentation, an additional COCO json file with + annotations referring to the cropped images will be written. + "visualization": Saves input images overlaid with object predictions. + padding : int, default=0 + This option allows to increase the cropping range beyond the predicted bounding box. + If possible, each side of the latter is shifted by the same number of pixels. + threshold : float or int, default=0.8 + Minimum score of predicted instances + seg_refinement : bool, default=False + If true, predictions are postprocessed with CascadePSP. + This option only works for instance segmentation. + refinement_device : str, default="cuda:0" + CPU or CUDA device for refinement with CascadePSP + refinement_method : str, default="full" + If set to "fast", the local refinement step will be skipped. + """ + self.d2_cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = threshold + self.d2_cfg.MODEL.RETINANET.SCORE_THRESH_TEST = threshold + + self._clear_coco("annotations") + self._clear_coco("annotations_cropped") + + # create/clean subdirectories of self.outdir + output_subdirs = [] + + if "cropped" in output_options: + output_subdirs.append("images_cropped") + if self.task == "instance-segmentation": + output_subdirs.append("masks_cropped") + if "visualization" in output_options: + output_subdirs.append("visualization") + + for subdir in output_subdirs: + target_dir = os.path.join(self.outdir, subdir) + os.makedirs(target_dir, exist_ok=True) + for path in glob.iglob(os.path.join(target_dir, "*")): + os.remove(path) + + # get image names + if not img_names: + patterns = ("*.jpg", "*.jpeg", "*.JPG", "*.JPEG") + img_paths = [] + for pat in patterns: + img_paths.extend(glob.glob(os.path.join(self.img_dir, pat))) + img_names = [os.path.split(path)[-1] for path in img_paths] + + # process images + with NamedTemporaryFile() as tmpfile: + + d2_predictor = DefaultPredictor(self.d2_cfg) + + # save predictions to disc + for i_img, img_name in enumerate(img_names): + + img_path = os.path.join(self.img_dir, img_name) + image = cv2.imread(img_path) + + predictions = d2_predictor(image) + + # convert to numpy arrays + boxes = predictions["instances"].get_fields()["pred_boxes"].to("cpu").tensor.numpy() + classes = predictions["instances"].get_fields()["pred_classes"].to("cpu").numpy() + scores = predictions["instances"].get_fields()["scores"].to("cpu").numpy() + + if self.task == "instance-segmentation": + masks = predictions["instances"].get_fields()["pred_masks"].to("cpu").numpy() + else: + masks = None # np.array([]) + + pickle.dump(boxes, tmpfile) + pickle.dump(classes, tmpfile) + pickle.dump(masks, tmpfile) + pickle.dump(scores, tmpfile) + + d2_predictor = None + torch.cuda.empty_cache() + + if self.task == "instance-segmentation" and seg_refinement: + refiner = refine.Refiner(device=refinement_device) + + tmpfile.seek(0) + + # load predictions and apply refinement (optional) + for i_img, img_name in enumerate(img_names): + + img_path = os.path.join(self.img_dir, img_name) + image = cv2.imread(img_path) + + boxes = pickle.load(tmpfile) + classes = pickle.load(tmpfile) + masks = pickle.load(tmpfile) + scores = pickle.load(tmpfile) + + if seg_refinement: + for i_mask, mask in enumerate(masks): + masks[i_mask] = refiner.refine( + image, + mask.astype("int") * 255, + fast = True if refinement_method=="fast" else False + ) + + if self.task == "instance-segmentation": + for i_mask, mask in enumerate(masks): + # recalculate bounding boxes + x_any = masks[i_mask].any(axis=0) + y_any = masks[i_mask].any(axis=1) + x = np.where(x_any == True)[0] + y = np.where(y_any == True)[0] + if len(x) > 0 and len(y) > 0: + boxes[i_mask] = [x[0], y[0], x[-1] + 1, y[-1] + 1] + else: + boxes[i_mask] = [0, 0, 0, 0] + + if "COCO" in output_options: + self._update_coco("annotations", image, img_name, i_img + 1, boxes, classes, masks) + + if "cropped" in output_options: + self._save_cropped(image, img_name, i_img + 1, boxes, classes, masks, padding) + + if "visualization" in output_options: + self._save_visualization(image, img_name, scores, classes, boxes, masks) + + if "COCO" in output_options: + self._save_coco("annotations") + self._clear_coco("annotations") + + if self.task == "instance-segmentation": + self._save_coco("annotations_cropped") + self._clear_coco("annotations_cropped") + + refiner = None + torch.cuda.empty_cache() + + + def _save_visualization( + self, + image: np.ndarray, + img_name: str, + scores: np.ndarray, + classes: np.ndarray, + boxes: np.ndarray, + masks: Optional[np.ndarray] = None + ): + + # not existing before + MetadataCatalog.get("pred").thing_classes = self.class_names + + visualizer = GJ_Visualizer( + image[:, :, ::-1], + metadata = MetadataCatalog.get("pred"), + instance_mode = ColorMode.IMAGE_BW + ) + vis_image = visualizer.draw_instance_predictions_gj(scores, classes, boxes, self.class_names, masks) + + outpath = os.path.join( + self.outdir, + "visualization", + img_name + ) + vis_image.save(outpath) + + + def _save_cropped( + self, + image: np.ndarray, + img_name: str, + img_id: int, + boxes: np.ndarray, + classes: List[str], + masks: Optional[np.ndarray] = None, + padding: int = 5 + ): + + height, width = image.shape[:2] + + for i_inst, bbox in enumerate(boxes): + + x1, y1, x2, y2 = [round(coord) for coord in bbox] + x1, x2 = np.clip((x1 - padding, x2 + padding), 0, width - 1) + y1, y2 = np.clip((y1 - padding, y2 + padding), 0, height - 1) + image_cropped = image[y1:y2, x1:x2] + + outpath = os.path.join( + self.outdir, + "images_cropped", + "{}_{}.jpg".format(os.path.splitext(img_name)[0], i_inst + 1) + ) + cv2.imwrite(outpath, image_cropped) + + if self.task == "instance-segmentation": + mask_cropped = masks[i_inst][y1:y2, x1:x2] + outpath = os.path.join( + self.outdir, + "masks_cropped", + "{}_{}.png".format(os.path.splitext(img_name)[0], i_inst + 1) + ) + cv2.imwrite(outpath, mask_cropped.astype("uint8") * 255) + + # prepare COCO annotation of cropped image: + + # calculate bounding box + x_any = mask_cropped.any(axis=0) + y_any = mask_cropped.any(axis=1) + x = np.where(x_any == True)[0] + y = np.where(y_any == True)[0] + if len(x) > 0 and len(y) > 0: + box = np.array([x[0], y[0], x[-1] + 1, y[-1] + 1]) + else: + box = np.array([0, 0, 0, 0]) + + self._update_coco( + "annotations_cropped", + image_cropped, + "img_{}_{}.jpg".format(img_id, i_inst + 1), + len(self._coco_images["annotations_cropped"]) + 1, + np.expand_dims(box, axis=0), + np.expand_dims(classes[i_inst], axis=0), + np.expand_dims(mask_cropped, axis=0) + ) + + + def _update_coco( + self, + name: str, + image: np.ndarray, + img_name: str, + img_id: int, + boxes: np.ndarray, + classes: List[str], + masks: Optional[np.ndarray] = None + ): + + height, width = image.shape[:2] + + self._coco_images[name].append({ + "id": img_id, + "file_name": img_name, + "coco_url": "", + "date_captured": "", + "flickr_url": "", + "height": height, + "width": width, + "license": 1 + }) + + for i_inst, bbox in enumerate(boxes): + bbox = bbox.tolist() + bbox_coco = [ bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1] ] + + anno = { + "area": (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), + "bbox": bbox_coco, + "iscrowd": 0, + "image_id": img_id, + "id": len(self._coco_annotations[name]) + 1, + "category_id": classes[i_inst].tolist() + 1 + } + + if self.task == "instance-segmentation": + imask = imantics.Mask(masks[i_inst]) + ipoly = imask.polygons() + # remove polygons with less than 3 points + anno["segmentation"] = [p for p in ipoly.segmentation if len(p) >= 6] + # recalculate bounding box + anno["bbox"] = bbox_from_polygons(anno["segmentation"], fmt="xywh").tolist() + anno["area"] = anno["bbox"][2] * anno["bbox"][3] + + self._coco_annotations[name].append(anno) + + + def _save_coco(self, name: str): + info = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + licenses = [{"id": 1, "name": "", "url": ""}] + categories = [ + {"id": i+1, "name": cl, "supercategory": ""} for (i, cl) in enumerate(self.class_names) + ] + + # write COCO annotation file + json_new = os.path.join(self.outdir, name + ".json") + with open(json_new, 'w') as json_file: + json.dump({ + 'info': info, + 'licenses': licenses, + 'images': self._coco_images[name], + 'annotations': self._coco_annotations[name], + 'categories': categories + }, + json_file, + indent=2, + sort_keys=True + ) + + + def _clear_coco(self, name: str): + self._coco_annotations[name] = [] + self._coco_images[name] = [] + + +class GJ_Visualizer(Visualizer): + """Modified version of Detectron2's Visualizer class. + """ + + def draw_instance_predictions_gj( + self, + scores: np.ndarray, + classes: np.ndarray, + boxes: np.ndarray, + class_names: List[str], + masks: Optional[np.ndarray] = None, + alpha: Union[int, float] = 0.2 + ) -> VisImage: + """ + Modification of Detectron2's draw_instance_predictions() using differently formatted inputs. + + Parameters + ---------- + scores : np.ndarray + Scores of predictions + classes : np.ndarray + Predicted classes + boxes : np.ndarray + Predicted bounding boxes as 3D array + class_names : list of str + Ordered list of object class names + masks : np.ndarray + Predicted segmentation masks as 3D array + alpha : float or int + Opacity of color mask applied to each segmentation instance, + should be within 0 and 1 + + Returns + ------- + VisImage + Image object with visualizations + """ + labels = _create_text_labels(classes, scores, class_names) + + colors = None + + if masks is not None: + img_bw = self.img.astype("f4").mean(axis=2) + img_bw = np.stack([img_bw] * 3, axis=2) + img_bw = img_bw.round().astype("int") + img_bw[masks.any(axis=0) > 0] = self.img[masks.any(axis=0) > 0] + self.output = VisImage(img_bw) + + self.overlay_instances( + masks = None if masks is None + else [GenericMask(x, self.output.height, self.output.width) for x in masks], + boxes = boxes, + labels = labels, + keypoints = None, + assigned_colors = colors, + alpha = alpha, + ) + return self.output diff --git a/ginjinn/predictor/tests/test_predictors.py b/ginjinn/predictor/tests/test_predictors.py new file mode 100644 index 0000000..96adbdf --- /dev/null +++ b/ginjinn/predictor/tests/test_predictors.py @@ -0,0 +1,8 @@ +''' Module for testing predictors.py +''' + +import pytest +from ginjinn.predictor import GinjinnPredictor + +def test_placeholder(): + pass \ No newline at end of file diff --git a/ginjinn/segmentation_refinement/LICENSE b/ginjinn/segmentation_refinement/LICENSE new file mode 100644 index 0000000..aa2407e --- /dev/null +++ b/ginjinn/segmentation_refinement/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Ho Kei Cheng, Jihoon Chung + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ginjinn/segmentation_refinement/NOTE.txt b/ginjinn/segmentation_refinement/NOTE.txt new file mode 100644 index 0000000..05619b3 --- /dev/null +++ b/ginjinn/segmentation_refinement/NOTE.txt @@ -0,0 +1,3 @@ +Paths were adapted to fit the GinJinn architecture. + +segmentation_refinement was developed by Ho Kei Cheng, Jihoon Chung and only adapted for GinJinn. See LICENSE. diff --git a/ginjinn/segmentation_refinement/__init__.py b/ginjinn/segmentation_refinement/__init__.py new file mode 100644 index 0000000..5ccf429 --- /dev/null +++ b/ginjinn/segmentation_refinement/__init__.py @@ -0,0 +1 @@ +from .main import Refiner \ No newline at end of file diff --git a/ginjinn/segmentation_refinement/download.py b/ginjinn/segmentation_refinement/download.py new file mode 100644 index 0000000..8776189 --- /dev/null +++ b/ginjinn/segmentation_refinement/download.py @@ -0,0 +1,30 @@ +import requests + +def download_file_from_google_drive(id, destination): + URL = "https://docs.google.com/uc?export=download" + + session = requests.Session() + + response = session.get(URL, params = { 'id' : id }, stream = True) + token = get_confirm_token(response) + + if token: + params = { 'id' : id, 'confirm' : token } + response = session.get(URL, params = params, stream = True) + + save_response_content(response, destination) + +def get_confirm_token(response): + for key, value in response.cookies.items(): + if key.startswith('download_warning'): + return value + + return None + +def save_response_content(response, destination): + CHUNK_SIZE = 32768 + + with open(destination, "wb") as f: + for chunk in response.iter_content(CHUNK_SIZE): + if chunk: # filter out keep-alive new chunks + f.write(chunk) diff --git a/ginjinn/segmentation_refinement/eval_helper.py b/ginjinn/segmentation_refinement/eval_helper.py new file mode 100644 index 0000000..8d9bf37 --- /dev/null +++ b/ginjinn/segmentation_refinement/eval_helper.py @@ -0,0 +1,192 @@ +import torch +import torch.nn.functional as F + +def resize_max_side(im, size, method): + h, w = im.shape[-2:] + max_side = max(h, w) + ratio = size / max_side + if method in ['bilinear', 'bicubic']: + return F.interpolate(im, scale_factor=ratio, mode=method, align_corners=False) + else: + return F.interpolate(im, scale_factor=ratio, mode=method) + +def safe_forward(model, im, seg, inter_s8=None, inter_s4=None): + """ + Slightly pads the input image such that its length is a multiple of 8 + """ + b, _, ph, pw = seg.shape + if (ph % 8 != 0) or (pw % 8 != 0): + newH = ((ph//8+1)*8) + newW = ((pw//8+1)*8) + p_im = torch.zeros(b, 3, newH, newW, device=im.device) + p_seg = torch.zeros(b, 1, newH, newW, device=im.device) - 1 + + p_im[:,:,0:ph,0:pw] = im + p_seg[:,:,0:ph,0:pw] = seg + im = p_im + seg = p_seg + + if inter_s8 is not None: + p_inter_s8 = torch.zeros(b, 1, newH, newW, device=im.device) - 1 + p_inter_s8[:,:,0:ph,0:pw] = inter_s8 + inter_s8 = p_inter_s8 + if inter_s4 is not None: + p_inter_s4 = torch.zeros(b, 1, newH, newW, device=im.device) - 1 + p_inter_s4[:,:,0:ph,0:pw] = inter_s4 + inter_s4 = p_inter_s4 + + images = model(im, seg, inter_s8, inter_s4) + return_im = {} + + for key in ['pred_224', 'pred_28_3', 'pred_56_2']: + return_im[key] = images[key][:,:,0:ph,0:pw] + del images + + return return_im + +def process_high_res_im(model, im, seg, L=900): + + stride = L//2 + + _, _, h, w = seg.shape + + """ + Global Step + """ + if max(h, w) > L: + im_small = resize_max_side(im, L, 'area') + seg_small = resize_max_side(seg, L, 'area') + elif max(h, w) < L: + im_small = resize_max_side(im, L, 'bicubic') + seg_small = resize_max_side(seg, L, 'bilinear') + else: + im_small = im + seg_small = seg + + images = safe_forward(model, im_small, seg_small) + + pred_224 = images['pred_224'] + pred_56 = images['pred_56_2'] + + """ + Local step + """ + + for new_size in [max(h, w)]: + im_small = resize_max_side(im, new_size, 'area') + seg_small = resize_max_side(seg, new_size, 'area') + _, _, h, w = seg_small.shape + + combined_224 = torch.zeros_like(seg_small) + combined_weight = torch.zeros_like(seg_small) + + r_pred_224 = (F.interpolate(pred_224, size=(h, w), mode='bilinear', align_corners=False)>0.5).float()*2-1 + r_pred_56 = F.interpolate(pred_56, size=(h, w), mode='bilinear', align_corners=False)*2-1 + + padding = 16 + step_size = stride - padding*2 + step_len = L + + used_start_idx = {} + for x_idx in range((w)//step_size+1): + for y_idx in range((h)//step_size+1): + + start_x = x_idx * step_size + start_y = y_idx * step_size + end_x = start_x + step_len + end_y = start_y + step_len + + # Shift when required + if end_y > h: + end_y = h + start_y = h - step_len + if end_x > w: + end_x = w + start_x = w - step_len + + # Bound x/y range + start_x = max(0, start_x) + start_y = max(0, start_y) + end_x = min(w, end_x) + end_y = min(h, end_y) + + # The same crop might appear twice due to bounding/shifting + start_idx = start_y*w + start_x + if start_idx in used_start_idx: + continue + else: + used_start_idx[start_idx] = True + + # Take crop + im_part = im_small[:,:,start_y:end_y, start_x:end_x] + seg_224_part = r_pred_224[:,:,start_y:end_y, start_x:end_x] + seg_56_part = r_pred_56[:,:,start_y:end_y, start_x:end_x] + + # Skip when it is not an interesting crop anyway + seg_part_norm = (seg_224_part>0).float() + high_thres = 0.9 + low_thres = 0.1 + if (seg_part_norm.mean() > high_thres) or (seg_part_norm.mean() < low_thres): + continue + grid_images = safe_forward(model, im_part, seg_224_part, seg_56_part) + grid_pred_224 = grid_images['pred_224'] + + # Padding + pred_sx = pred_sy = 0 + pred_ex = step_len + pred_ey = step_len + + if start_x != 0: + start_x += padding + pred_sx += padding + if start_y != 0: + start_y += padding + pred_sy += padding + if end_x != w: + end_x -= padding + pred_ex -= padding + if end_y != h: + end_y -= padding + pred_ey -= padding + + combined_224[:,:,start_y:end_y, start_x:end_x] += grid_pred_224[:,:,pred_sy:pred_ey,pred_sx:pred_ex] + + del grid_pred_224 + + # Used for averaging + combined_weight[:,:,start_y:end_y, start_x:end_x] += 1 + + # Final full resolution output + seg_norm = (r_pred_224/2+0.5) + pred_224 = combined_224 / combined_weight + pred_224 = torch.where(combined_weight==0, seg_norm, pred_224) + + _, _, h, w = seg.shape + images = {} + images['pred_224'] = F.interpolate(pred_224, size=(h, w), mode='bilinear', align_corners=True) + + return images['pred_224'] + + +def process_im_single_pass(model, im, seg, L=900): + """ + A single pass version, aka global step only. + """ + + _, _, h, w = im.shape + if max(h, w) < L: + im = resize_max_side(im, L, 'bicubic') + seg = resize_max_side(seg, L, 'bilinear') + + if max(h, w) > L: + im = resize_max_side(im, L, 'area') + seg = resize_max_side(seg, L, 'area') + + images = safe_forward(model, im, seg) + + if max(h, w) < L: + images['pred_224'] = F.interpolate(images['pred_224'], size=(h, w), mode='area') + elif max(h, w) > L: + images['pred_224'] = F.interpolate(images['pred_224'], size=(h, w), mode='bilinear', align_corners=True) + + return images['pred_224'] diff --git a/ginjinn/segmentation_refinement/main.py b/ginjinn/segmentation_refinement/main.py new file mode 100644 index 0000000..653f9b1 --- /dev/null +++ b/ginjinn/segmentation_refinement/main.py @@ -0,0 +1,76 @@ +import os + +import numpy as np +import torch +from torchvision import transforms + +from .models.psp.pspnet import RefinementModule +from .eval_helper import process_high_res_im, process_im_single_pass +from .download import download_file_from_google_drive + + +class Refiner: + def __init__(self, device='cpu', model_folder=None): + """ + Initialize the segmentation refinement model. + device can be 'cpu' or 'cuda' + model_folder specifies the folder in which the model will be downloaded and stored. Defaulted in ~/.segmentation-refinement. + """ + self.model = RefinementModule() + self.device = device + if model_folder is None: + model_folder = os.path.expanduser("~/.segmentation-refinement") + + if not os.path.exists(model_folder): + os.makedirs(model_folder, exist_ok=True) + + model_path = os.path.join(model_folder, 'model') + if not os.path.exists(model_path): + print('Downloading the model file into: %s...' % model_path) + download_file_from_google_drive('103nLN1JQCs2yASkna0HqfioYZO7MA_J9', model_path) + + model_dict = torch.load(model_path, map_location={'cuda:0': device}) + new_dict = {} + for k, v in model_dict.items(): + name = k[7:] # Remove module. from dataparallel + new_dict[name] = v + self.model.load_state_dict(new_dict) + self.model.eval().to(device) + + self.im_transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ), + ]) + + self.seg_transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize( + mean=[0.5], + std=[0.5] + ), + ]) + + def refine(self, image, mask, fast=False, L=900): + with torch.no_grad(): + """ + Refines an input segmentation mask of the image. + + image should be of size [H, W, 3]. Range 0~255. + Mask should be of size [H, W] or [H, W, 1]. Range 0~255. We will make the mask binary by thresholding at 127. + Fast mode - Use the global step only. Default: False. The speedup is more significant for high resolution images. + L - Hyperparameter. Setting a lower value reduces memory usage. In fast mode, a lower L will make it runs faster as well. + """ + image = self.im_transform(image).unsqueeze(0).to(self.device) + mask = self.seg_transform((mask>127).astype(np.uint8)*255).unsqueeze(0).to(self.device) + if len(mask.shape) < 4: + mask = mask.unsqueeze(0) + + if fast: + output = process_im_single_pass(self.model, image, mask, L) + else: + output = process_high_res_im(self.model, image, mask, L) + + return (output[0,0].cpu().numpy()*255).astype('uint8') diff --git a/ginjinn/segmentation_refinement/models/__init__.py b/ginjinn/segmentation_refinement/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ginjinn/segmentation_refinement/models/psp/__init__.py b/ginjinn/segmentation_refinement/models/psp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ginjinn/segmentation_refinement/models/psp/extractors.py b/ginjinn/segmentation_refinement/models/psp/extractors.py new file mode 100644 index 0000000..0f6315e --- /dev/null +++ b/ginjinn/segmentation_refinement/models/psp/extractors.py @@ -0,0 +1,108 @@ +from collections import OrderedDict +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def conv3x3(in_planes, out_planes, stride=1, dilation=1): + return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, + padding=dilation, dilation=dilation, bias=False) + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, inplanes, planes, stride=1, downsample=None, dilation=1): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, dilation=dilation, + padding=dilation, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False) + self.bn3 = nn.BatchNorm2d(planes * 4) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Module): + def __init__(self, block, layers=(3, 4, 23, 3)): + self.inplanes = 64 + super(ResNet, self).__init__() + self.conv1 = nn.Conv2d(6, 64, kernel_size=7, stride=2, padding=3, + bias=False) + self.bn1 = nn.BatchNorm2d(64) + self.relu = nn.ReLU(inplace=True) + self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0]) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2) + self.layer3 = self._make_layer(block, 256, layers[2], stride=1, dilation=2) + self.layer4 = self._make_layer(block, 512, layers[3], stride=1, dilation=4) + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + m.weight.data.normal_(0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2d): + m.weight.data.fill_(1) + m.bias.data.zero_() + + def _make_layer(self, block, planes, blocks, stride=1, dilation=1): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2d(self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias=False), + nn.BatchNorm2d(planes * block.expansion), + ) + + layers = [block(self.inplanes, planes, stride, downsample)] + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes, dilation=dilation)) + + return nn.Sequential(*layers) + + def forward(self, x): + x_1 = self.conv1(x) # /2 + x = self.bn1(x_1) + x = self.relu(x) + x = self.maxpool(x) # /2 + + x_2 = self.layer1(x) + x = self.layer2(x_2) # /2 + x = self.layer3(x) + x = self.layer4(x) + + return x, x_1, x_2 + + +def resnet50(): + model = ResNet(Bottleneck, [3, 4, 6, 3]) + return model + diff --git a/ginjinn/segmentation_refinement/models/psp/pspnet.py b/ginjinn/segmentation_refinement/models/psp/pspnet.py new file mode 100644 index 0000000..f02c964 --- /dev/null +++ b/ginjinn/segmentation_refinement/models/psp/pspnet.py @@ -0,0 +1,171 @@ +import torch +from torch import nn +from torch.nn import functional as F + +from ginjinn.segmentation_refinement.models.psp import extractors + + +class PSPModule(nn.Module): + def __init__(self, features, out_features=1024, sizes=(1, 2, 3, 6)): + super().__init__() + self.stages = [] + self.stages = nn.ModuleList([self._make_stage(features, size) for size in sizes]) + self.bottleneck = nn.Conv2d(features * (len(sizes) + 1), out_features, kernel_size=1) + self.relu = nn.ReLU(inplace=True) + + def _make_stage(self, features, size): + prior = nn.AdaptiveAvgPool2d(output_size=(size, size)) + conv = nn.Conv2d(features, features, kernel_size=1, bias=False) + return nn.Sequential(prior, conv) + + def forward(self, feats): + h, w = feats.size(2), feats.size(3) + set_priors = [F.interpolate(input=stage(feats), size=(h, w), mode='bilinear', align_corners=False) for stage in self.stages] + priors = set_priors + [feats] + bottle = self.bottleneck(torch.cat(priors, 1)) + return self.relu(bottle) + + +class PSPUpsample(nn.Module): + def __init__(self, x_channels, in_channels, out_channels): + super().__init__() + self.conv = nn.Sequential( + nn.BatchNorm2d(in_channels), + nn.ReLU(inplace=True), + nn.Conv2d(in_channels, out_channels, 3, padding=1), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, 3, padding=1), + ) + + self.conv2 = nn.Sequential( + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, 3, padding=1), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, 3, padding=1), + ) + + self.shortcut = nn.Conv2d(x_channels, out_channels, kernel_size=1) + + def forward(self, x, up): + x = F.interpolate(input=x, scale_factor=2, mode='bilinear', align_corners=False) + + p = self.conv(torch.cat([x, up], 1)) + sc = self.shortcut(x) + + p = p + sc + + p2 = self.conv2(p) + + return p + p2 + + +class RefinementModule(nn.Module): + def __init__(self): + super().__init__() + + self.feats = extractors.resnet50() + self.psp = PSPModule(2048, 1024, (1, 2, 3, 6)) + + self.up_1 = PSPUpsample(1024, 1024+256, 512) + self.up_2 = PSPUpsample(512, 512+64, 256) + self.up_3 = PSPUpsample(256, 256+3, 32) + + self.final_28 = nn.Sequential( + nn.Conv2d(1024, 32, kernel_size=1), + nn.ReLU(inplace=True), + nn.Conv2d(32, 1, kernel_size=1), + ) + + self.final_56 = nn.Sequential( + nn.Conv2d(512, 32, kernel_size=1), + nn.ReLU(inplace=True), + nn.Conv2d(32, 1, kernel_size=1), + ) + + self.final_11 = nn.Conv2d(32+3, 32, kernel_size=1) + self.final_21 = nn.Conv2d(32, 1, kernel_size=1) + + def forward(self, x, seg, inter_s8=None, inter_s4=None): + + images = {} + + """ + First iteration, s8 output + """ + if inter_s8 is None: + p = torch.cat((x, seg, seg, seg), 1) + + f, f_1, f_2 = self.feats(p) + p = self.psp(f) + + inter_s8 = self.final_28(p) + r_inter_s8 = F.interpolate(inter_s8, scale_factor=8, mode='bilinear', align_corners=False) + r_inter_tanh_s8 = torch.tanh(r_inter_s8) + + images['pred_28'] = torch.sigmoid(r_inter_s8) + images['out_28'] = r_inter_s8 + else: + r_inter_tanh_s8 = inter_s8 + + """ + Second iteration, s8 output + """ + if inter_s4 is None: + p = torch.cat((x, seg, r_inter_tanh_s8, r_inter_tanh_s8), 1) + + f, f_1, f_2 = self.feats(p) + p = self.psp(f) + inter_s8_2 = self.final_28(p) + r_inter_s8_2 = F.interpolate(inter_s8_2, scale_factor=8, mode='bilinear', align_corners=False) + r_inter_tanh_s8_2 = torch.tanh(r_inter_s8_2) + + p = self.up_1(p, f_2) + + inter_s4 = self.final_56(p) + r_inter_s4 = F.interpolate(inter_s4, scale_factor=4, mode='bilinear', align_corners=False) + r_inter_tanh_s4 = torch.tanh(r_inter_s4) + + images['pred_28_2'] = torch.sigmoid(r_inter_s8_2) + images['out_28_2'] = r_inter_s8_2 + images['pred_56'] = torch.sigmoid(r_inter_s4) + images['out_56'] = r_inter_s4 + else: + r_inter_tanh_s8_2 = inter_s8 + r_inter_tanh_s4 = inter_s4 + + """ + Third iteration, s1 output + """ + p = torch.cat((x, seg, r_inter_tanh_s8_2, r_inter_tanh_s4), 1) + + f, f_1, f_2 = self.feats(p) + p = self.psp(f) + inter_s8_3 = self.final_28(p) + r_inter_s8_3 = F.interpolate(inter_s8_3, scale_factor=8, mode='bilinear', align_corners=False) + + p = self.up_1(p, f_2) + inter_s4_2 = self.final_56(p) + r_inter_s4_2 = F.interpolate(inter_s4_2, scale_factor=4, mode='bilinear', align_corners=False) + p = self.up_2(p, f_1) + p = self.up_3(p, x) + + + """ + Final output + """ + p = F.relu(self.final_11(torch.cat([p, x], 1)), inplace=True) + p = self.final_21(p) + + pred_224 = torch.sigmoid(p) + + images['pred_224'] = pred_224 + images['out_224'] = p + images['pred_28_3'] = torch.sigmoid(r_inter_s8_3) + images['pred_56_2'] = torch.sigmoid(r_inter_s4_2) + images['out_28_3'] = r_inter_s8_3 + images['out_56_2'] = r_inter_s4_2 + + return images diff --git a/ginjinn/simulation/__init__.py b/ginjinn/simulation/__init__.py new file mode 100644 index 0000000..9b1dca7 --- /dev/null +++ b/ginjinn/simulation/__init__.py @@ -0,0 +1,4 @@ +'''Module for simulating simple data sets +''' + +from .simulation import generate_simple_shapes_coco, generate_simple_shapes_pvoc diff --git a/ginjinn/simulation/coco_utils.py b/ginjinn/simulation/coco_utils.py new file mode 100644 index 0000000..d239773 --- /dev/null +++ b/ginjinn/simulation/coco_utils.py @@ -0,0 +1,222 @@ +'''Utility functions for working with COCO data sets. +''' + +from typing import List + +def build_coco_dataset( + annotations: List[dict], + images: List[dict], + categories: List[dict], + licenses: List[dict], + info: dict +): + '''Construct COCO data set dictionary. + + Parameters + ---------- + annotations : List[dict] + List of annotations + images : List[dict] + List of images + categories : List[dict] + List of categories + licenses : List[dict] + List of licenses + info : dict + Info dictionary + + Returns + ------- + dict + COCO data set dictionary. + ''' + return { + 'annotations': annotations, + 'images': images, + 'categories': categories, + 'licenses': licenses, + 'info': info, + } + +def build_coco_annotation( #pylint: disable=too-many-arguments + ann_id: int, + image_id: int, + category_id: int, + bbox: List[float], + segmentation: List[float], + area: float, + iscrowd: int = 0 +): + '''Construct COCO annotation dictionary. + + Parameters + ---------- + ann_id : int + Annotation ID + image_id : int + Image ID + category_id : int + Category ID + bbox : List[float] + xmin, ymin, width, height + segmentation : List[float] + x1, y1, x2, y2, ..., xn, yn + area : float + Annotated area. + iscrowd : int, optional + Whether the annotation is a crowd, by default 0 + + Returns + ------- + dict + COCO annotation dictionary. + ''' + return { + 'id': int(ann_id), + 'bbox': bbox, + 'image_id': int(image_id), + 'segmentation': segmentation, + 'category_id': int(category_id), + 'area': float(area), + 'iscrowd': int(iscrowd) + } + +def build_coco_category( + category_id: int, + name: str, + supercategory: str = '', +): + '''Construct COCO category dictionary. + + Parameters + ---------- + category_id : int + Category ID + name : str + Category name + supercategory : str, optional + Name of the supercategory, by default '' + + Returns + ------- + dict + COCO category dictionary. + ''' + return { + 'id': category_id, + 'supercategory': supercategory, + 'name': name, + } + +def build_coco_image( #pylint: disable=too-many-arguments,redefined-builtin + image_id: int, + file_name: str, + width: int, + height: int, + license: int = 0, + coco_url: str = '', + date_captured: int = 0, + flickr_url: str = '' +): + '''Construct COCO image dictionary + + Parameters + ---------- + image_id : int + Image ID + file_name : str + Image file name/path + width : int + Image width in pixel + height : int + Image height in pixel + license : int, optional + License ID, by default 0 + coco_url : str, optional + COCO URL, by default '' + date_captured : int, optional + Date, by default 0 + flickr_url : str, optional + Flickr URL, by default '' + + Returns + ------- + dict + COCO image dictionary + ''' + return { + 'id': int(image_id), + 'file_name': file_name, + 'width': width, + 'height': height, + 'license': license, + 'date_captured': date_captured, + 'coco_url': coco_url, + 'flickr_url': flickr_url + } + +def build_coco_license( + license_id: int, + name: str = '', + url: str = '', +): + '''Construct COCO license dictionary. + + Parameters + ---------- + license_id : int + License ID + name : str, optional + License name, by default '' + url : str, optional + License URL, by default '' + + Returns + ------- + dict + COCO license dictionary + ''' + return { + 'name': name, + 'url': url, + 'id': license_id, + } + +def build_coco_info( #pylint: disable=too-many-arguments + version: str = '', + year: str = '', + description: str = '', + url: str = '', + date_created: str = '', + contributor: str = '', +): + '''Construct COCO info dictionary + + Parameters + ---------- + version: str, optional + version, by default ' + year : str, optional + year, by default '' + description : str, optional + description, by default '' + url : str, optional + url, by default '' + date_created : str, optional + date_created, by default '' + contributor : str, optional + contributor, by default '' + + Returns + ------- + dict + COCO description dictionary + ''' + return { + 'version': version, + 'year': year, + 'description': description, + 'url': url, + 'date_created': date_created, + 'contributor': contributor + } diff --git a/ginjinn/simulation/pvoc_utils.py b/ginjinn/simulation/pvoc_utils.py new file mode 100644 index 0000000..cfd838d --- /dev/null +++ b/ginjinn/simulation/pvoc_utils.py @@ -0,0 +1,126 @@ +'''Utility functions for working with PVOC data sets. +''' + +from typing import List +import xml.etree.ElementTree as ET + +def build_pvoc_object( #pylint: disable=too-many-arguments + category: str, + bbox: List[int], + truncated: int = 0, + difficult: int = 0, + pose: str = 'Unspecified', +): + '''build_pvoc_object + + Construct a PVOC object XML (xml.etree.ElementTree.ElementTree). + + Parameters + ---------- + category : str + Object category. + bbox : List[int] + List (or np.array) of [xmin, ymin, xmax, ymax]. + truncated : int, optional + Whether the object is truncated, by default 0. Ignored by GinJinn. + difficult : int, optional + Whether the object is difficult, by default 0. + pose : str, optional + Object pose, by default 'Unspecified'. Ignore by GinJinn + + Returns + ------- + PVOC_XML + XML (xml.etree.ElementTree.ElementTree) representation of the + PVOC object. + ''' + + ann_object = ET.Element('object') + obj_name = ET.SubElement(ann_object, 'name') + obj_name.text = str(category) + obj_pose = ET.SubElement(ann_object, 'pose') + obj_pose.text = str(pose) + obj_truncated = ET.SubElement(ann_object, 'truncated') + obj_truncated.text = str(truncated) + obj_difficult = ET.SubElement(ann_object, 'difficult') + obj_difficult.text = str(difficult) + obj_bbox = ET.SubElement(ann_object, 'bndbox') + bbox_xmin = ET.SubElement(obj_bbox, 'xmin') + bbox_xmin.text = str(bbox[0]) + bbox_ymin = ET.SubElement(obj_bbox, 'ymin') + bbox_ymin.text = str(bbox[1]) + bbox_xmax = ET.SubElement(obj_bbox, 'xmax') + bbox_xmax.text = str(bbox[2]) + bbox_ymax = ET.SubElement(obj_bbox, 'ymax') + bbox_ymax.text = str(bbox[3]) + + return ann_object + +def build_pvoc_annotation( #pylint: disable=too-many-arguments + folder: str, + file_name: str, + path: str, + img_size: List[int], + objects: List, + segmented: int = 0, + database_source: str = 'Unknown', + verified: str = 'yes', +): + '''build_pvoc_annotation + + Construct a PVOC annotation XML (xml.etree.ElementTree.ElementTree). + + Parameters + ---------- + folder : str + Image folder relative to PVOC project. + file_name : str + Image file name. + path : str + Image file path. Ignored by GinJinn. + img_size : List[int] + List (or np.array) of [width, height, depth]. + objects : List + PVOC objects as XML (xml.etree.ElementTree.ElementTree). + segmented : int, optional + Whether the object is segmented, by default 0. Ignored by GinJinn. + database_source : str, optional + Database source string, by default 'Unknown'. Ignored by GinJinn + verified : str, optional + Whether the annotation was verified, by default 'yes'. Ignored by GinJinn. + + Returns + ------- + PVOC_XML + XML (xml.etree.ElementTree.ElementTree) representation of the + PVOC annotation. + ''' + + ann = ET.Element('annotation', {'verified': verified}) + + img_folder = ET.SubElement(ann, 'folder') + img_folder.text = str(folder) + img_file_name = ET.SubElement(ann, 'filename') + img_file_name.text = str(file_name) + img_path = ET.SubElement(ann, 'path') + img_path.text = str(path) + + source = ET.SubElement(ann, 'source') + database = ET.SubElement(source, 'database') + database.text = str(database_source) + + size = ET.SubElement(ann, 'size') + width = ET.SubElement(size, 'width') + width.text = str(img_size[0]) + height = ET.SubElement(size, 'height') + height.text = str(img_size[1]) + depth = ET.SubElement(size, 'depth') + depth.text = str(img_size[2]) + + obj_segmented = ET.SubElement(ann, 'segmented') + obj_segmented.text = str(segmented) + + for ann_object in objects: + ann.append(ann_object) + + return ann diff --git a/ginjinn/simulation/shapes.py b/ginjinn/simulation/shapes.py new file mode 100644 index 0000000..7d9f0c0 --- /dev/null +++ b/ginjinn/simulation/shapes.py @@ -0,0 +1,211 @@ +'''Module for generating random shapes. +''' + +# from typing import Optional, Tuple + +import numpy as np +import skimage.draw +import skimage.measure +import skimage.filters + +try: + draw_circle = skimage.draw.disk +except: # pylint: disable=bare-except + def draw_circle(center, radius, shape=None): + '''Wrapper around skimage.draw.circle for backward compatibility, + when skimage.draw.disk is not available + + Parameters + ---------- + center + center of the circle + radius + radius of the circle + shape, optional + shape of "image" to draw to, by default None + + Returns + ------- + Circle + Numpy "image" containing a circle + ''' + return skimage.draw.circle(center[0], center[1], radius, shape) + +# Should probably find/clip contours using +# https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm +# instead of using skimage. + +def add_circle( + img: np.ndarray, + c_xy: np.ndarray, + r, + col=(1,1,1) +): + '''Add circle to image. + + This function modfies img in place. + + Parameters + ---------- + img : np.ndarray + Image to be modified as numpy array. + c_xy : np.ndarray + Center coordinates of the circle + r : float + Radius of the circle + col : tuple, optional + Color of the circle, by default white: (1,1,1) + + Returns + ------- + Tuple + Tuple of circle data: (contour, center_xy, radius) + ''' + + # circle_coords = skimage.draw.disk(c_xy[[1,0]], radius=r, shape=img.shape) + circle_coords = draw_circle(c_xy[[1,0]], radius=r, shape=img.shape) + + + img[circle_coords] = col + + # need to add border for find_contours to work at the corners of the image + tmp_img = np.zeros((img.shape[0] + 2, img.shape[1] + 2), dtype=np.int32) + tmp_img[(circle_coords[0] + 1, circle_coords[1] + 1)] = 1 + + circle_contour = sorted(skimage.measure.find_contours(tmp_img, level=0), key=len)[0] - 1 + + return circle_contour[:,[1,0]], c_xy, r + +def add_random_circle( + img: np.ndarray, + r_min, + r_max, + col=(1,1,1) +): + '''Add random circle to image + + Parameters + ---------- + img : np.ndarray + Image to be modified as numpy array. + r_min : float + Minimum radius of the circle + r_max : float + Maximum radius of the circle + col : tuple, optional + Color of the circle, by default white: (1,1,1) + + Returns + ------- + Tuple + Tuple of circle data: (contour, center_xy, radius) + ''' + h, w, _ = img.shape + r = np.random.randint(r_min, r_max) + c_x = np.random.randint(r, w) + c_y = np.random.randint(r, h) + c_xy = np.array([c_x, c_y]) + + return add_circle(img, c_xy, r, col) + +def add_triangle( + img: np.ndarray, + c_xy: np.ndarray, + r, + rot=0.0, + col=(1,1,1) +): + '''Add equilateral triangle to image + + Parameters + ---------- + img : np.ndarray + Image to be modified as numpy array. + c_xy : np.ndarray + Center coordinates of the triangle + r : float + Radius of the triangle + rot : float, optional + Rotation of the triangle in degrees, by default 0.0 + col : tuple, optional + Color of the triangle, by default white: (1,1,1) + + Returns + ------- + Tuple + Tuple of triangle data: (contour, center_xy, radius, rotation) + ''' + rot_mat_120 = np.array([ + [-1/2, -np.sqrt(3)/2], + [np.sqrt(3)/2, -1/2] + ]) + + rot = rot * np.pi / 180 + rot_mat = np.array([ + [np.cos(rot), -np.sin(rot)], + [np.sin(rot), np.cos(rot)] + ]) + + # vertices a, b, c of the triangle + a = np.dot(rot_mat, np.array([0, r])) + b = np.dot(rot_mat_120, a) + c = np.dot(rot_mat_120, b) + + rows = np.array([a[1], b[1], c[1]]) + c_xy[1] + cols = np.array([a[0], b[0], c[0]]) + c_xy[0] + + triangle_coords = skimage.draw.polygon(rows, cols, shape=img.shape) + img[triangle_coords] = col + + # need to add border for find_contours to work at the corners of the image + tmp_img = np.zeros((img.shape[0] + 2, img.shape[1] + 2)) + tmp_img[(triangle_coords[0] + 1, triangle_coords[1] + 1)] = 1 + tmp_img = skimage.filters.gaussian(tmp_img, 0.25) + triangle_contour = sorted(skimage.measure.find_contours(tmp_img, level=0), key=len)[0] - 1 + + + return triangle_contour[:,[1,0]], c_xy, r, rot + + +def add_random_triangle( #pylint: disable=too-many-arguments + img: np.ndarray, + r_min, + r_max, + rot_min=0.0, + rot_max=0.0, + col=(1,1,1) +): + '''Add random triangle to image + + Parameters + ---------- + img : np.ndarray + Image to be modified as numpy array. + r_min : float + Minimum radius of the triangle + r_max : float + Maximum radius of the triangle + rot_min : float, optional + Minimum rotation of the triangle in degrees, by default 0.0 + rot_max : float, optional + Maximum rotation of the triangle in degrees, by default 0.0 + col : tuple, optional + Color of the triangle, by default white: (1,1,1) + + Returns + ------- + Tuple + Tuple of triangle data: (contour, center_xy, radius, rotation) + ''' + h, w, _ = img.shape + r = np.random.randint(r_min, r_max) + c_x = np.random.randint(r, w) + c_y = np.random.randint(r, h) + c_xy = np.array([c_x, c_y]) + + if rot_min == rot_max: + rot = rot_min + else: + rot = np.random.randint(rot_min, rot_max) + + return add_triangle(img, c_xy, r, rot, col) diff --git a/ginjinn/simulation/simulation.py b/ginjinn/simulation/simulation.py new file mode 100644 index 0000000..7017b4c --- /dev/null +++ b/ginjinn/simulation/simulation.py @@ -0,0 +1,279 @@ +'''Module for simulating simple data sets +''' + +import os +import json +import xml.etree.ElementTree as ET +from xml.dom import minidom +import skimage.filters +import skimage.util +import skimage.io +import numpy as np + +from .coco_utils import \ + build_coco_annotation, build_coco_category, build_coco_dataset,\ + build_coco_image, build_coco_info, build_coco_license +from .pvoc_utils import build_pvoc_annotation, build_pvoc_object +from .shapes import add_random_circle, add_random_triangle +from .utils import polygon_area + +def generate_simple_shapes_coco( #pylint: disable=too-many-arguments,too-many-locals + img_dir: str, + ann_file: str, + n_images: int = 100, + min_w: int = 400, + max_w: int = 800, + min_h: int = 400, + max_h: int = 800, + min_n_shapes: int = 1, + max_n_shapes: int = 4, + circle_col: np.ndarray = np.array([0.8, 0.5, 0.5]), + triangle_col: np.ndarray = np.array([0.5, 0.8, 0.5]), + col_var: float = 0.15, + min_r: float = 25, + max_r: float = 75, + min_rot: float = 0.0, + max_rot: float = 60.0, + noise: float = 0.005, + triangle_prob: float = 0.5, +): + '''Generate a simple COCO data set. + + Parameters + ---------- + img_dir : str + Path to directory, where images should be stored. Must exist. + ann_file : str + Path to annotation file (output). + n_images : int, optional + Number of images to generate, by default 100 + min_w : int, optional + Minimum image width, by default 400 + max_w : int, optional + Maximum image width, by default 800 + min_h : int, optional + Minimum image height, by default 400 + max_h : int, optional + Maximum image height, by default 800 + min_n_shapes : int, optional + Minumum number of shapes per image, by default 1 + max_n_shapes : int, optional + Maximum number of shapes per image, by default 4 + circle_col : np.ndarray, optional + Mean circle color, by default np.array([0.8, 0.5, 0.5]) + triangle_col : np.ndarray, optional + Mean triangle color, by default np.array([0.5, 0.8, 0.5]) + col_var : float, optional + Variance of colors, by default 0.15 + min_r : float, optional + Minimum shape radius, by default 25 + max_r : float, optional + Maximum shape radius, by default 75 + min_rot : float, optional + Minimum shape rotation, by default 0.0 + max_rot : float, optional + Maximum shape rotation, by default 60.0 + noise : float, optional + Amount of gaussian noise to add to image, by default 0.005 + triangle_prob : float, optional + Probability of drawin a triangle, by default 0.5 + ''' + category_map = { + 'circle': 1, + 'triangle': 2, + } + + annotations = [] + images = [] + categories = [ + build_coco_category(_id, name, '') for name, _id in category_map.items() + ] + licenses = [build_coco_license(0)] + info = build_coco_info() + + ann_id = 0 + + for i in range(n_images): + w, h = np.random.randint(min_w, max_w + 1), np.random.randint(min_h, max_h + 1) + img = np.full((h, w, 3), (0.8, 0.8, 0.8)) + + img_id = i + 1 + file_name = os.path.join(img_dir, f'img_{img_id}.jpg') + images.append(build_coco_image( + img_id, os.path.basename(file_name), w, h + )) + + for _ in range(np.random.randint(min_n_shapes, max_n_shapes)): + ann_id +=1 + + if np.random.uniform() > triangle_prob: + col = np.clip(np.random.normal(circle_col, scale=col_var), 0, 1) + contour, *_ = add_random_circle(img, min_r, max_r, col=col) + category = 'circle' + else: + col = np.clip(np.random.normal(triangle_col, scale=col_var), 0, 1) + contour, *_ = add_random_triangle( + img, min_r, max_r, rot_min=min_rot, rot_max=max_rot, col=col + ) + category = 'triangle' + bbox = np.array([*np.min(contour, 0), *np.max(contour, 0)]) + bbox = np.array([bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]]) + area = polygon_area(contour[:,0], contour[:,1]) + cat_id = category_map[category] + + annotations.append(build_coco_annotation( + ann_id, img_id, cat_id, + list(bbox), + [list(contour.flatten())], + area, + 0 + )) + + # add noise + img = skimage.filters.gaussian(img, sigma=1, multichannel=True) + img = skimage.util.random_noise(img, var=noise) + img = skimage.filters.gaussian(img, sigma=0.25, multichannel=True) + img *= 255 + img = img.astype(np.uint8) + skimage.io.imsave(file_name, img) + + dataset = build_coco_dataset( + annotations, + images, + categories, + licenses, + info + ) + + with open(ann_file, 'w') as ann_f: + json.dump(dataset, ann_f) + +def generate_simple_shapes_pvoc( #pylint: disable=too-many-arguments,too-many-locals + img_dir: str, + ann_dir: str, + n_images: int = 100, + min_w: int = 400, + max_w: int = 800, + min_h: int = 400, + max_h: int = 800, + min_n_shapes: int = 1, + max_n_shapes: int = 4, + circle_col: np.ndarray = np.array([0.8, 0.5, 0.5]), + triangle_col: np.ndarray = np.array([0.5, 0.8, 0.5]), + col_var: float = 0.15, + min_r: float = 25, + max_r: float = 75, + min_rot: float = 0.0, + max_rot: float = 60.0, + noise: float = 0.005, + triangle_prob: float = 0.5, + encoding: str = 'utf-8', +): + '''Generate a simple PVOC data set. + + Parameters + ---------- + img_dir : str + Path to directory, where images should be stored. Must exist. + ann_dir : str + Path to directory, where annotations should be stored. Must exist. + n_images : int, optional + Number of images to generate, by default 100 + min_w : int, optional + Minimum image width, by default 400 + max_w : int, optional + Maximum image width, by default 800 + min_h : int, optional + Minimum image height, by default 400 + max_h : int, optional + Maximum image height, by default 800 + min_n_shapes : int, optional + Minumum number of shapes per image, by default 1 + max_n_shapes : int, optional + Maximum number of shapes per image, by default 4 + circle_col : np.ndarray, optional + Mean circle color, by default np.array([0.8, 0.5, 0.5]) + triangle_col : np.ndarray, optional + Mean triangle color, by default np.array([0.5, 0.8, 0.5]) + col_var : float, optional + Variance of colors, by default 0.15 + min_r : float, optional + Minimum shape radius, by default 25 + max_r : float, optional + Maximum shape radius, by default 75 + min_rot : float, optional + Minimum shape rotation, by default 0.0 + max_rot : float, optional + Maximum shape rotation, by default 60.0 + noise : float, optional + Amount of gaussian noise to add to image, by default 0.005 + triangle_prob : float, optional + Probability of drawin a triangle, by default 0.5 + encoding : str, optional + XML encoding, by default 'utf-8'. + ''' + annotations = [] + images = [] + + for i in range(n_images): + objects = [] + + w, h = np.random.randint(min_w, max_w + 1), np.random.randint(min_h, max_h + 1) + img = np.full((h, w, 3), (0.8, 0.8, 0.8)) + + img_id = i + 1 + file_name = os.path.join(img_dir, f'img_{img_id}.jpg') + images.append(file_name) + + for _ in range(np.random.randint(min_n_shapes, max_n_shapes)): + if np.random.uniform() > triangle_prob: + col = np.clip(np.random.normal(circle_col, scale=col_var), 0, 1) + contour, *_ = add_random_circle(img, min_r, max_r, col=col) + category = 'circle' + else: + col = np.clip(np.random.normal(triangle_col, scale=col_var), 0, 1) + contour, *_ = add_random_triangle( + img, min_r, max_r, rot_min=min_rot, rot_max=max_rot, col=col + ) + category = 'triangle' + bbox = np.array( + [*np.clip(np.min(contour, 0), 0, w-1), *np.clip(np.max(contour, 0), 0, h-1)] + ).astype(np.int) + bbox = np.array(bbox) + + objects.append(build_pvoc_object( + category=category, + bbox=bbox, + truncated=0, + difficult=0, + pose='unspecified' + )) + + annotations.append(build_pvoc_annotation( + folder='images', + file_name=os.path.basename(file_name), + path=os.path.abspath(file_name), + img_size=np.array([w, h, 3]), + objects=objects, + segmented=0, + database_source='Unknown', + verified='yes', + )) + + # add noise + img = skimage.filters.gaussian(img, sigma=1, multichannel=True) + img = skimage.util.random_noise(img, var=noise) + img = skimage.filters.gaussian(img, sigma=0.25, multichannel=True) + img *= 255 + img = img.astype(np.uint8) + skimage.io.imsave(file_name, img) + + for ann_id, ann in enumerate(annotations): + file_name = os.path.join(ann_dir, f'img_{ann_id + 1}.xml') + + with open(file_name, 'w') as xml_f: + xml_f.write( + minidom.parseString( + ET.tostring(ann, encoding=encoding) + ).toprettyxml(indent=' ') + ) diff --git a/ginjinn/simulation/tests/test_simulation.py b/ginjinn/simulation/tests/test_simulation.py new file mode 100644 index 0000000..2227ed5 --- /dev/null +++ b/ginjinn/simulation/tests/test_simulation.py @@ -0,0 +1,60 @@ +''' Tests for data simulation +''' + +import os +import tempfile +import pytest + +from ginjinn.simulation import generate_simple_shapes_coco, generate_simple_shapes_pvoc + +@pytest.fixture(scope='module', autouse=True) +def tmp_dir(): + tmpdir = tempfile.TemporaryDirectory() + + yield tmpdir.name + + tmpdir.cleanup() + +def test_simple_shapes_coco(tmp_dir): + test_dir = os.path.join(tmp_dir, 'test_simple_shapes_coco') + os.mkdir(test_dir) + + img_dir = os.path.join(test_dir, 'images') + os.mkdir(img_dir) + + ann_file = os.path.join(test_dir, 'annotations.json') + generate_simple_shapes_coco( + img_dir, + ann_file, + n_images=10 + ) + + generate_simple_shapes_coco( + img_dir, + ann_file, + n_images=10, + min_rot=0, max_rot=0 + ) + +def test_simple_shapes_pvoc(tmp_dir): + test_dir = os.path.join(tmp_dir, 'test_simple_shapes_pvoc') + os.mkdir(test_dir) + + img_dir = os.path.join(test_dir, 'images') + os.mkdir(img_dir) + + ann_dir = os.path.join(test_dir, 'annotations') + os.mkdir(ann_dir) + + generate_simple_shapes_pvoc( + img_dir, + ann_dir, + n_images=10 + ) + + generate_simple_shapes_pvoc( + img_dir, + ann_dir, + n_images=10, + min_rot=0, max_rot=0 + ) \ No newline at end of file diff --git a/ginjinn/simulation/utils.py b/ginjinn/simulation/utils.py new file mode 100644 index 0000000..54343fc --- /dev/null +++ b/ginjinn/simulation/utils.py @@ -0,0 +1,27 @@ +'''Module for simulation utilites +''' + +import numpy as np + +def polygon_area(x: np.ndarray, y: np.ndarray) -> np.ndarray: + '''Calculate polygon area from ordered vertex coordinates + + Parameters + ---------- + x : np.ndarray + X coordinates + y : np.ndarray + Y coordinates + + Returns + ------- + np.ndarray + Area of the polygon. + ''' + # coordinate shift + x_shift = x - x.mean() + y_shift = y - y.mean() + # everything else is the same as maxb's code + correction = x_shift[-1] * y_shift[0] - y_shift[-1]* x_shift[0] + main_area = np.dot(x_shift[:-1], y_shift[1:]) - np.dot(y_shift[:-1], x_shift[1:]) + return 0.5*np.abs(main_area + correction) diff --git a/ginjinn/test_pytest.py b/ginjinn/test_pytest.py new file mode 100644 index 0000000..6e89e18 --- /dev/null +++ b/ginjinn/test_pytest.py @@ -0,0 +1,11 @@ +''' A simple test +''' + +def test_pytest(): + '''test_pytest + + Summary + ------- + Dummy test function. + ''' + assert True diff --git a/ginjinn/tests/test_main.py b/ginjinn/tests/test_main.py new file mode 100644 index 0000000..a20e3ef --- /dev/null +++ b/ginjinn/tests/test_main.py @@ -0,0 +1,12 @@ +''' Test main +''' + +import pytest +import subprocess + +def test_main_simple(): + p = subprocess.Popen('ginjinn') + r = p.communicate() + rc = p.returncode + + assert rc == 0 diff --git a/ginjinn/trainer/__init__.py b/ginjinn/trainer/__init__.py new file mode 100644 index 0000000..e71c06d --- /dev/null +++ b/ginjinn/trainer/__init__.py @@ -0,0 +1,4 @@ +''' GinJinn Trainer module +''' + +from .trainer import ValTrainer, Trainer diff --git a/ginjinn/trainer/tests/test_trainer.py b/ginjinn/trainer/tests/test_trainer.py new file mode 100644 index 0000000..401f776 --- /dev/null +++ b/ginjinn/trainer/tests/test_trainer.py @@ -0,0 +1,119 @@ +import pytest +import sys +import copy +import tempfile +import os +import mock +import pkg_resources +import yaml + +from detectron2.data import DatasetCatalog + +from ginjinn.ginjinn_config import GinjinnConfiguration +from ginjinn.simulation import generate_simple_shapes_coco +from ginjinn.data_reader.load_datasets import load_train_val_sets + +from ginjinn.trainer.trainer import ValTrainer, Trainer + +@pytest.fixture(scope='module', autouse=True) +def tmp_dir(): + tmpdir = tempfile.TemporaryDirectory() + + yield tmpdir.name + + tmpdir.cleanup() + +@pytest.fixture(scope='module') +def simulate_coco_train(tmp_dir): + sim_dir = os.path.join(tmp_dir, 'sim_coco_train') + os.mkdir(sim_dir) + + img_dir = os.path.join(sim_dir, 'images') + os.mkdir(img_dir) + ann_path = os.path.join(sim_dir, 'annotations.json') + generate_simple_shapes_coco( + img_dir=img_dir, ann_file=ann_path, n_images=40, + ) + return img_dir, ann_path + +@pytest.fixture(scope='module') +def simulate_coco_validation(tmp_dir): + sim_dir = os.path.join(tmp_dir, 'sim_coco_validation') + os.mkdir(sim_dir) + + img_dir = os.path.join(sim_dir, 'images') + os.mkdir(img_dir) + ann_path = os.path.join(sim_dir, 'annotations.json') + generate_simple_shapes_coco( + img_dir=img_dir, ann_file=ann_path, n_images=20, + ) + return img_dir, ann_path + +@pytest.fixture(scope='module', autouse=True) +def example_config(tmp_dir, simulate_coco_train, simulate_coco_validation): + img_dir_train, ann_path_train = simulate_coco_train + img_dir_validation, ann_path_validation = simulate_coco_validation + + example_config_1_path = pkg_resources.resource_filename( + 'ginjinn', 'data/ginjinn_config/example_config_1.yaml', + ) + + with open(example_config_1_path) as config_f: + config = yaml.load(config_f) + + config['input']['training']['annotation_path'] = ann_path_train + config['input']['training']['image_path'] = img_dir_train + + config['input']['validation'] = {} + config['input']['validation']['annotation_path'] = ann_path_validation + config['input']['validation']['image_path'] = img_dir_validation + + config['augmentation'] = [aug for aug in config['augmentation'] if not 'crop' in list(aug.keys())[0]] + # config['augmentation'] = [config['augmentation'][0]] + + config['training']['max_iter'] = 100 + + config_dir = os.path.join(tmp_dir, 'example_config') + os.mkdir(config_dir) + os.mkdir(os.path.join(config_dir, 'outputs')) + config['project_dir'] = os.path.abspath(config_dir) + + config_file = os.path.join(config_dir, 'ginjinn_config.yaml') + with open(config_file, 'w') as config_f: + yaml.dump(config, config_f) + + return (config, config_file) + +def test_trainer(example_config): + _, config_file = example_config + config = GinjinnConfiguration.from_config_file(config_file) + + print(config) + + try: + DatasetCatalog.remove('train') + except: + pass + try: + DatasetCatalog.remove('val') + except: + pass + + load_train_val_sets(config) + + try: + trainer = ValTrainer.from_ginjinn_config(config) + trainer.resume_or_load(resume=False) + trainer.train() + except AssertionError as err: + if 'NVIDIA driver' in str(err): + Warning(str(err)) + else: + raise err + except RuntimeError as err: + if 'NVIDIA driver' in str(err): + Warning(str(err)) + else: + raise err + except Exception as err: + raise err \ No newline at end of file diff --git a/ginjinn/trainer/trainer.py b/ginjinn/trainer/trainer.py new file mode 100644 index 0000000..66479a4 --- /dev/null +++ b/ginjinn/trainer/trainer.py @@ -0,0 +1,510 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# Modified by AGOberprieler, 2020, using modifications made by Marcelo Ortega: +# https://gist.github.com/ortegatron/c0dad15e49c2b74de8bb09a5615d9f6b + +""" +Classes for training and, optionally, simultaneous validation. +""" + +import copy +import datetime +import logging +import os +import time +import json +import re +from typing import List, Union +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.backends.backend_pdf import PdfPages +import torch +import numpy as np +from detectron2.data import build_detection_train_loader, build_detection_test_loader +from detectron2.data import DatasetMapper +from detectron2.data import detection_utils +from detectron2.data import transforms as T +from detectron2.config import CfgNode +from detectron2.engine.defaults import DefaultTrainer +from detectron2.engine.hooks import HookBase +from detectron2.evaluation import COCOEvaluator +from detectron2.utils.logger import log_every_n_seconds +from detectron2.utils import comm +from ginjinn.ginjinn_config import GinjinnConfiguration + +class Trainer(DefaultTrainer): + """Trainer class which allows to set the applied augmentations at runtime. + """ + _augmentations = [] + + @classmethod + def set_augmentations(cls, augmentations): + """Specify augmentations for training. + + Parameters + ---------- + augmentations : list + Augmentations to be applied + """ + cls._augmentations = augmentations + + @classmethod + def build_train_loader(cls, cfg: CfgNode): + """Build data loader for training. + + Parameters + ---------- + cfg : CfgNode + Detectron2 config. + + Returns + ---------- + torch.utils.data.DataLoader + Data loader + """ + augs = [T.ResizeShortestEdge( + cfg.INPUT.MIN_SIZE_TRAIN, + cfg.INPUT.MAX_SIZE_TRAIN, + cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING + )] + augs.extend(cls._augmentations) + + return build_detection_train_loader( + cfg, + mapper=DatasetMapper( + cfg, + is_train=True, + augmentations=augs + ) + ) + + @classmethod + def from_ginjinn_config(cls, gj_cfg : GinjinnConfiguration) -> "Trainer": + '''from_ginjinn_config + + Build Trainer object from GinjinnConfiguration instead of + detectron2 configuration. + + Parameters + ---------- + gj_cfg : GinjinnConfiguration + GinjinnConfiguration object. + + Returns + ------- + Trainer + Trainer object + ''' + + cls.set_augmentations(gj_cfg.augmentation.to_detectron2_augmentations()) + + detectron2_cfg = gj_cfg.to_detectron2_config() + + return cls(detectron2_cfg) + + ##alternative: + #@classmethod + #def build_train_loader(cls, cfg): + #"""Build data loader for training. + + #Returns + #---------- + #torch.utils.data.DataLoader + #Data loader + #""" + #augs = [T.ResizeShortestEdge( + #cfg.INPUT.MIN_SIZE_TRAIN, + #cfg.INPUT.MAX_SIZE_TRAIN, + #cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING + #)] + #augs.extend(cls._augmentations) + + #return build_detection_train_loader( + #cfg, + #mapper = lambda data_dict: mapper_train(data_dict, augs) + #) + + def build_hooks(self): + hooks = super().build_hooks() + hooks.append(PlottingHook(self.cfg.TEST.EVAL_PERIOD, self.cfg.OUTPUT_DIR)) + return hooks + + +class ValTrainer(Trainer): + """This trainer class evaluates validation data during training. + """ + @classmethod + def build_evaluator(cls, cfg: CfgNode, dataset_name: str) -> COCOEvaluator: + """Builds COCO evaluator for a given dataset. + + Parameters + ---------- + cfg : CfgNode + Detectron2 config. + dataset_name : str + Name of the evaluation data set. + + Returns + ---------- + COCOEvaluator + """ + output_folder = os.path.join(cfg.OUTPUT_DIR, "inference") + return COCOEvaluator(dataset_name, cfg, True, output_folder) + + def build_hooks(self): + hooks = super().build_hooks() + hooks.insert(-2, LossEvalHook( + self.cfg.TEST.EVAL_PERIOD, + self.model, + build_detection_test_loader( + self.cfg, + self.cfg.DATASETS.TEST[0], + DatasetMapper( + self.cfg, + is_train=True, # required to obtain losses + # no flip + augmentations=[T.ResizeShortestEdge( + self.cfg.INPUT.MIN_SIZE_TRAIN, + self.cfg.INPUT.MAX_SIZE_TRAIN, + self.cfg.INPUT.MIN_SIZE_TRAIN_SAMPLING + )] + ) + ) + )) + hooks.append(PlottingHook(self.cfg.TEST.EVAL_PERIOD, self.cfg.OUTPUT_DIR)) + return hooks + + +def mapper_train(dataset_dict: dict, augmentations: list): + """ + This basic mapper function takes a dataset dictionary in Detectron2 format, + and maps it to a format used by the model. + + Parameters + ---------- + dataset_dict : dict + Annotations for one image in Detectron2 format + augmentations : list + Augmentations and transformations to be applied + + Returns + ------- + dict + Format accepted by builtin models in Detectron2 + """ + dataset_dict = copy.deepcopy(dataset_dict) + image = detection_utils.read_image(dataset_dict["file_name"], format="BGR") + detection_utils.check_image_size(dataset_dict, image) + + image, transforms = T.apply_transform_gens(augmentations, image) + + dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32")) # pylint: disable=E1101 + + annos = [ + detection_utils.transform_instance_annotations(obj, transforms, image.shape[:2]) + for obj in dataset_dict.pop("annotations") + if obj.get("iscrowd", 0) == 0 + ] + instances = detection_utils.annotations_to_instances(annos, image.shape[:2]) + dataset_dict["instances"] = detection_utils.filter_empty_instances(instances) + return dataset_dict + + +class LossEvalHook(HookBase): + # pylint: disable=E1101 + """ + This hook allows periodic loss calculation for the validation data set. + It is executed every ``eval_period`` iterations and after the last iteration. + + Parameters + ---------- + eval_period : int + Period to calculate losses. If set to 0, they are only calculated after the + last iteration. + model : torch.nn.Module + Model to be used + data_loader : iterable + produces data to be run by `model(data)` + """ + def __init__(self, eval_period: int, model: torch.nn.Module, data_loader): + self._model = model + self._period = eval_period + self._data_loader = data_loader + + def _do_loss_eval(self): + # see evaluator.py from Detectron2 + total = len(self._data_loader) + num_warmup = min(5, total - 1) + + start_time = time.perf_counter() + total_compute_time = 0 + losses_all = {} + for idx, inputs in enumerate(self._data_loader): + if idx == num_warmup: + start_time = time.perf_counter() + total_compute_time = 0 + start_compute_time = time.perf_counter() + if torch.cuda.is_available(): + torch.cuda.synchronize() + total_compute_time += time.perf_counter() - start_compute_time + iters_after_start = idx + 1 - num_warmup * int(idx >= num_warmup) + seconds_per_img = total_compute_time / iters_after_start + if idx >= num_warmup * 2 or seconds_per_img > 5: + total_seconds_per_img = (time.perf_counter() - start_time) / iters_after_start + eta = datetime.timedelta(seconds=int(total_seconds_per_img * (total - idx - 1))) + log_every_n_seconds( + logging.INFO, + "Loss on Validation done {}/{}. {:.4f} s / img. ETA={}".format( + idx + 1, total, seconds_per_img, str(eta) + ), + n=5, + ) + losses_batch = self._get_loss(inputs) + if losses_all: + for key in losses_batch: + losses_all[key].append(losses_batch[key]) + else: + for key in losses_batch: + losses_all[key] = [losses_batch[key]] + + losses_mean = {key + "_val": np.mean(values) for (key, values) in losses_all.items()} + losses_mean["total_loss_val"] = sum(losses_mean.values()) + self.trainer.storage.put_scalars(**losses_mean, smoothing_hint=False) + + comm.synchronize() + return losses_mean + + def _get_loss(self, data): + metrics_dict = self._model(data) + metrics_dict = { + k: v.detach().cpu().item() if isinstance(v, torch.Tensor) else float(v) + for k, v in metrics_dict.items() + } + return metrics_dict + + def after_step(self): + next_iter = self.trainer.iter + 1 + is_final = next_iter == self.trainer.max_iter + if is_final or (self._period > 0 and next_iter % self._period == 0): + self._do_loss_eval() + + +class PlottingHook(HookBase): + """ + This hook provides periodic plotting of losses and evaluation scores. + It is executed every ``eval_period`` iterations and after the last iteration. + + Parameters + ---------- + eval_period : int + Period to plot losses and evaluation scores. If set to 0, they are only calculated + after the last iteration. + outdir : str + Output directory + """ + def __init__(self, eval_period: int, outdir: str): + self.period = eval_period + self.outdir = outdir + self.metrics_df = None + self.pp = None + + def after_step(self): + next_iter = self.trainer.iter + 1 + is_final = next_iter == self.trainer.max_iter + if is_final or (self.period > 0 and next_iter % self.period == 0): + self._plot_all() + + def _plot_all(self): + """Plot all metrics logged in metrics.json into metrics.pdf. + """ + json_file = os.path.join(self.outdir, "metrics.json") + if not os.path.isfile(json_file): + return + + entries = [] + with open(json_file, 'r') as f: + for line in f: + entry = json.loads(line) + entries.append(entry) + + self.metrics_df = pd.DataFrame.from_records(entries).sort_values(by='iteration') + colnames = self.metrics_df.columns.tolist() + + plt.ioff() + self.pp = PdfPages(os.path.join(self.outdir, "metrics.pdf")) + + # plot losses + p = re.compile(".*loss.*$") + cols_sel = [s for s in colnames if p.match(s)] + for col in cols_sel: + colnames.remove(col) + self._plot_losses(cols_sel, smooth=True) + + # plot evaluation scores (bbox) + p = re.compile("^bbox/.*$") + cols_sel = [s for s in colnames if p.match(s)] + for col in cols_sel: + colnames.remove(col) + self._plot_metrics( + cols_sel, + nrow_grid = 2, + ncol_grid = 3, + dataset_name = "val", + legend_pos = "lower right" + ) + + # plot evaluation scores (segmentation) + p = re.compile("^segm/.*$") + cols_sel = [s for s in colnames if p.match(s)] + for col in cols_sel: + colnames.remove(col) + self._plot_metrics( + cols_sel, + nrow_grid = 2, + ncol_grid = 3, + dataset_name = "val", + legend_pos = "lower right" + ) + + # plot remaining metrics + self._plot_metrics(colnames, nrow_grid=3, ncol_grid=4) + + self.pp.close() + + def _plot_metrics( + self, + cols: List[str], + nrow_grid: int = 2, + ncol_grid: int = 3, + width: Union[float, int] = 11.69, + height: Union[float, int] = 8.27, + dataset_name: str = None, + legend_pos: str = "best" + ): + """Plot multiple metrics, arranged by a specfied grid. + If the grid size is not sufficient to accomodate all subplots, additional pages + are appended to the resulting PDF. + + Parameters + ---------- + cols : list of str + Names of data frame columns to be plotted + nrow_grid : int + Number of rows per PDF page + ncol_grid : int + Number of columns per PDF page + width : float or int + Page width in inches, defaults to A4 (landscape) + height : float or int + Page height in inches, defaults to A4 (landscape) + dataset_name : str + If specified, this is used as legend text. + legend_pos : str + Legend position, see matplotlib.pyplot.legend, + e.g., "upper right", "lower left", "best", etc. + """ + if not cols: + return + grid_size = nrow_grid * ncol_grid + for i in range(0, len(cols), grid_size): + cols_chunk = cols[i:i+grid_size] + fig, axs = plt.subplots(nrow_grid, ncol_grid) + fig.set_size_inches(width, height) + for ax, metric_name in zip(axs.flat, cols_chunk): + idcs = ~self.metrics_df[metric_name].isna() + ax.plot( + self.metrics_df['iteration'][idcs], + self.metrics_df[metric_name][idcs], + label = dataset_name + ) + ax.set_title(metric_name) + if dataset_name: + ax.legend(loc=legend_pos) + for ax in axs.flat: + if not ax.lines: + ax.axis("off") + fig.tight_layout() + self.pp.savefig() + + def _plot_losses( + self, + cols: List[str], + nrow_grid: int = 2, + ncol_grid: int = 3, + width: Union[float, int] = 11.69, + height: Union[float, int] = 8.27, + legend_pos: str = "best", + smooth: bool = False, + window_size: int = 10 + ): + """Plot multiple losses, arranged by a specfied grid. + In case a validation data set is used, its scores are combined with those of + the training data set. If the grid size is not sufficient to accomodate all + subplots, additional pages are appended to the resulting PDF. + + Parameters + ---------- + cols : list of str + Names of data frame columns (losses) to be plotted + nrow_grid : int + Number of rows per PDF page + ncol_grid : int + Number of columns per PDF page + width : float or int + Page width in inches, defaults to A4 (landscape) + height : float or int + Page height in inches, defaults to A4 (landscape) + legend_pos : str + Legend position, see matplotlib.pyplot.legend, + e.g., "upper right", "lower left", "best", etc. + smooth : bool + If set to True, training losses are overlaid with their rolling mean. + window_size : int + Number of values to be averaged if smooth is set to True. + """ + if not cols: + return + p = re.compile("^.*_val$") + cols_train = [c for c in cols if not p.match(c)] + grid_size = nrow_grid * ncol_grid + for i in range(0, len(cols_train), grid_size): + cols_chunk = cols_train[i:i+grid_size] + fig, axs = plt.subplots(nrow_grid, ncol_grid) + fig.set_size_inches(width, height) + for ax, metric_name in zip(axs.flat, cols_chunk): + idcs = ~self.metrics_df[metric_name].isna() + if smooth: + ax.plot( + self.metrics_df['iteration'][idcs], + self.metrics_df[metric_name][idcs], + label="train", + color="tab:blue", + alpha=0.3 + ) + ax.plot( + self.metrics_df['iteration'][idcs], + self.metrics_df[metric_name][idcs].rolling(window_size).mean(), + label="train (smoothed)", + color="tab:blue" + ) + else: + ax.plot( + self.metrics_df['iteration'][idcs], + self.metrics_df[metric_name][idcs], + label="train", + color="tab:blue" + ) + if metric_name + "_val" in cols: + idcs = ~self.metrics_df[metric_name + "_val"].isna() + ax.plot( + self.metrics_df['iteration'][idcs], + self.metrics_df[metric_name + "_val"][idcs], + label="val", + color="tab:orange" + ) + ax.set_title(metric_name) + ax.legend(loc=legend_pos, fontsize="small") + for ax in axs.flat: + if not ax.lines: + ax.axis("off") + fig.tight_layout() + self.pp.savefig() diff --git a/ginjinn/utils/__init__.py b/ginjinn/utils/__init__.py new file mode 100644 index 0000000..b08835a --- /dev/null +++ b/ginjinn/utils/__init__.py @@ -0,0 +1,9 @@ +'''GinJinn utilities +''' + +from .utils import \ + confirmation_cancel, confirmation, \ + load_coco_ann, get_image_files, get_obj_anns + +from .data_prep import flatten_coco +from .dataset_cropping import crop_seg_from_coco, crop_bbox_from_coco diff --git a/ginjinn/utils/data_prep.py b/ginjinn/utils/data_prep.py new file mode 100644 index 0000000..642e78e --- /dev/null +++ b/ginjinn/utils/data_prep.py @@ -0,0 +1,384 @@ +''' Module containing functionality for data set preprocessing. +''' + +import glob +import json +import os +import shutil +from typing import List +import xml.etree.ElementTree as ET +import datetime +import cv2 +import imantics +import pandas as pd +from .utils import bbox_from_mask, coco_seg_to_mask, confirmation + +def flatten_coco( + ann_file: str, + img_root_dir: str, + out_dir: str, + sep: str = '~', + custom_id: bool = False, + annotated_only: bool = False, + link_images: bool = True +): + '''flatten_coco + + Flatten COCO data set in such a way that all images are located in the same + directory. + + Parameters + ---------- + ann_file : str + Path to annotation (JSON) file. + img_root_dir : str + Root dir of the image directory. For COCO, this directory is often called + "images". + out_dir : str + Output directory. + sep : str, optional + Seperator for path flattening, by default '~' + custom_id : bool, optional + Whether the new image name should be replaced with a custom id, by default False + annotated_only : bool, optional + Whether only annotated images should be kept in the data set. + link_images : bool, optional + If true, images won't be copied but hard-linked instead. + ''' + with open(ann_file) as ann_f: + annotations = json.load(ann_f) + + out_img_dir = os.path.join(out_dir, 'images') + if not os.path.exists(out_img_dir): + os.mkdir(out_img_dir) + + if annotated_only: + img_ids = {ann['image_id'] for ann in annotations['annotations']} + annotations['images'] = [ann for ann in annotations['images'] if ann['id'] in img_ids] + + id_map = {} + + for i, img_ann in enumerate(annotations['images']): + file_name = img_ann['file_name'] + + if custom_id: + new_file_name = f'{i}.jpg' + id_map[i] = file_name + else: + new_file_name = file_name.replace('/', sep) + + img_ann['file_name'] = new_file_name + if link_images: + os.link( + os.path.join(img_root_dir, file_name), + os.path.join(out_img_dir, new_file_name) + ) + else: + shutil.copy( + os.path.join(img_root_dir, file_name), + os.path.join(out_img_dir, new_file_name) + ) + + out_ann_file = os.path.join(out_dir, 'annotations.json') + with open(out_ann_file, 'w') as ann_f: + json.dump(annotations, ann_f, indent=2) + + if custom_id: + id_map_file = os.path.join(out_dir, 'id_map.csv') + id_map_df = pd.DataFrame.from_records( + list(id_map.items()), + columns=['id', 'original_path'] + ) + id_map_df.to_csv(id_map_file, index=False) + + +def filter_categories_coco( + ann_file: str, + img_dir: str, + out_dir: str, + link_images: bool = True, + drop: List[str] = None, + keep: List[str] = None +): + """filter_categories_coco + + This function allows to filter object annotations in a COCO dataset according to their + assigned category. Therefore, either ``drop`` or ``keep`` has to be specified. + If img_dir is specified, a new, filtered image directory is created as well. + Note that the original IDs referring to annotations, categories and images are preserved, + and may be non-contiguous in the output dataset. + + Parameters + ---------- + ann_file : str + Path to annotation (JSON) file + img_dir: str + Directory containing image files. If None, no filtered image directory is written. + out_dir: str + Output directory + link_images : bool + If true, images won't be copied but hard-linked instead. + drop : list of str + If specified, these categories are removed from the dataset. + keep : list of str + If specified, only these categories are preserved. + + Raises + ------ + ValueError + Raised for unsupported parameter settings. + """ + if bool(drop) + bool(keep) == 0: + raise ValueError( + "Either ``drop`` or ``keep`` has to be specified as non-empty list." + ) + + if os.path.exists(out_dir): + if confirmation( + out_dir + ' already exists.\nDo you want do overwrite it?' + ): + shutil.rmtree(out_dir) + else: + return + + os.makedirs(out_dir) + if img_dir: + os.makedirs(os.path.join(out_dir, "images")) + + with open(ann_file, "rb") as f: + anno = json.load(f) + + if keep: + categories_left = {cat["id"] for cat in anno.get("categories") if cat["name"] in keep} + else: + categories_left = {cat["id"] for cat in anno.get("categories") if cat["name"] not in drop} + + anno["annotations"] = [ann for ann in anno.get("annotations") if ann["category_id"] in categories_left] + anno["categories"] = [cat for cat in anno.get("categories") if cat["id"] in categories_left] + + images_left = {ann["image_id"] for ann in anno.get("annotations")} + anno["images"] = [img for img in anno.get("images") if img["id"] in images_left] + + anno["info"] = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + + # write COCO json file + with open(os.path.join(out_dir, "annotations.json"), 'w') as json_file: + json.dump( + anno, + json_file, + indent = 2, + sort_keys = True + ) + + if img_dir: + for img in anno.get("images"): + if img["id"] in images_left: + img_name = os.path.split(img["file_name"])[1] + img_path = os.path.join(img_dir, img_name) + + if link_images: + os.link(img_path, os.path.join(out_dir, "images", img_name)) + else: + shutil.copy(img_path, os.path.join(out_dir, "images", img_name)) + + +def filter_categories_pvoc( + ann_dir: str, + img_dir: str, + out_dir: str, + link_images: bool = True, + drop: List[str] = None, + keep: List[str] = None +): + """filter_categories_pvoc + + This function allows to filter object annotations in a PascalVOC dataset according to their + assigned category. Therefore, either ``drop`` or ``keep`` has to be specified. + If img_dir is specified, a new, filtered image directory is created as well. + + Parameters + ---------- + ann_dir: str + Directory containing annotation files (XML) + img_dir: str + Directory containing image files. If None, no filtered image directory is written. + out_dir: str + Output directory + link_images : bool + If true, images won't be copied but hard-linked instead. + drop : list of str + If specified, these categories are removed from the dataset. + keep : list of str + If specified, only these categories are preserved. + + Raises + ------ + ValueError + Raised for unsupported parameter settings. + """ + if bool(drop) + bool(keep) == 0: + raise ValueError( + "Either ``drop`` or ``keep`` has to be specified as non-empty list." + ) + + if os.path.exists(out_dir): + if confirmation( + out_dir + ' already exists.\nDo you want do overwrite it?' + ): + shutil.rmtree(out_dir) + else: + return + + os.makedirs(os.path.join(out_dir, "annotations")) + if img_dir: + os.makedirs(os.path.join(out_dir, "images")) + + for ann_path in glob.glob(os.path.join(ann_dir, "*.xml")): + tree = ET.parse(ann_path) + root = tree.getroot() + + for child in root.findall("object"): + if keep and child.findtext("name") not in keep: + root.remove(child) + if drop and child.findtext("name") in drop: + root.remove(child) + + if root.findall("object"): + tree.write( + os.path.join( + out_dir, + "annotations", + os.path.split(ann_path)[1] + ) + ) + if img_dir: + img_name = os.path.split(root.findtext("filename"))[1] + img_path = os.path.join(img_dir, img_name) + + if link_images: + os.link(img_path, os.path.join(out_dir, "images", img_name)) + else: + shutil.copy(img_path, os.path.join(out_dir, "images", img_name)) + +def filter_objects_by_size( + ann_file: str, + out_file: str, + task: str, + min_width: int = 0, + min_height: int = 0, + min_area: int = 0, + min_fragment_area: int = 0 +): + """filter_objects_by_size + + Filter (sub)objects from a COCO annotation file by size. + + Parameters + ---------- + ann_file : str + Path to annotation (JSON) file + out_file : str + Output file name + task : str + Either 'bbox-detection' or 'instance-segmentation', determines whether filter criteria + are applied to bounding boxes or segmentations. + min_width : int + Min. object width (pixels) + min_height : int + Min. object height (pixels) + min_area : int + Min. object area (pixels) + min_fragment_area : int + Min. area of object parts (pixels). + If a segmentation instance consists of multiple disjunct parts, this option allows + to remove small subobjects without discarding the whole object. + + Raises + ------ + ValueError + Raised for invalid filter settings. + """ + if ( + max(min_width, min_height, min_area, min_fragment_area) < 1 + or min(min_width, min_height, min_area, min_fragment_area) < 0 + ): + raise ValueError( + "\"min_width\", \"min_height\", \"min_area\", and \"min_fragment_area\" have to be "\ + "non-negative integers, at least one of which must be non-zero." + ) + + with open(ann_file, "rb") as f: + ann = json.load(f) + + dict_images = {img_ann["id"]: img_ann for img_ann in ann.get("images")} + annotations_filtered = [] + + for annotation in ann.get("annotations"): + img_ann = dict_images[annotation["image_id"]] + img_width = img_ann["width"] + img_height = img_ann["height"] + + if task == "bbox-detection": + obj_width, obj_height = annotation["bbox"][2:] + if ( + obj_width >= min_width + and obj_height >= min_height + and obj_width * obj_height >= min_area + ): + annotations_filtered.append(annotation) + + elif task == "instance-segmentation": + seg = annotation.get("segmentation") + if seg: + seg_mask = coco_seg_to_mask(seg, img_width, img_height) + if seg_mask.sum() < min_area: + continue + else: + continue + + # calculate width and height from segmentation + *_, obj_width, obj_height = bbox_from_mask(seg_mask, fmt="xywh").tolist() + if obj_width < min_width or obj_height < min_height: + continue + + if min_fragment_area > 0: + n_labels, labels, stats, _ = cv2.connectedComponentsWithStats( + seg_mask.astype("uint8"), + connectivity = 8 + ) + sizes = stats[-1] + # 0th element: background + for i in range(1, n_labels): + if sizes[i] < min_fragment_area: + seg_mask[labels == i] = False + annotation["segmentation"] = imantics.Mask(seg_mask).polygons().segmentation + annotation["iscrowd"] = 0 + + if seg_mask.sum() > 0: + annotations_filtered.append(annotation) + + ann["annotations"] = annotations_filtered + ann["info"] = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + + # write COCO json file + with open(out_file, 'w') as json_file: + json.dump( + ann, + json_file, + indent = 2, + sort_keys = True + ) diff --git a/ginjinn/utils/dataset_cropping.py b/ginjinn/utils/dataset_cropping.py new file mode 100644 index 0000000..aee523f --- /dev/null +++ b/ginjinn/utils/dataset_cropping.py @@ -0,0 +1,1045 @@ +""" +Module for generating datasets with cropped object instances. +""" + +from collections import defaultdict +import datetime +import glob +import json +import math +import os +import copy +import xml +from typing import Generator, List, Sequence, Tuple, Optional +import numpy as np +#from numpy.typing import DTypeLike +import cv2 +import imantics +from pycocotools import mask +from ginjinn.simulation import coco_utils +from .utils import load_coco_ann, get_obj_anns +from .utils import get_pvoc_obj_bbox, bbox_from_mask,\ + bbox_from_polygons, crop_bbox, bbox_size, set_pvoc_obj_bbox,\ + drop_pvoc_objects, get_pvoc_filename, set_pvoc_filename,\ + get_pvoc_objects, add_pvoc_object, set_pvoc_size, get_pvoc_size,\ + load_pvoc_annotation, write_pvoc_annotation, coco_seg_to_mask + + +def sw_coords_1d(length: int, win_length: int, overlap: int) -> Generator[Tuple[int], None, None]: + """sw_coords_1d + + Generate start and stop indices for sliding window cropping with padding. + + Parameters + ---------- + length : int + Width or length of image to be cropped + win_length : int + Width or length of sliding windows + overlap : int + Absolute horizontal or vertical overlap (pixels) between neighboring windows. + + Yields + ------ + (start, stop) : tuple of int + Start and stop indices. Negative start values or stop values above length indicate padding. + """ + n_windows = math.ceil((length - overlap) / (win_length - overlap)) + excess = n_windows * win_length - (n_windows - 1) * overlap - length + + start = -round(excess/2) + for _ in range(n_windows): + yield (start, start + win_length) + start += win_length - overlap + +def crop_img_padded( + img: np.ndarray, + cropping_range: Sequence[int], + dtype: Optional["DTypeLike"] = None +) -> np.ndarray: + """Crop image or mask with padding. + + Parameters + ---------- + img : np.ndarray + Input image/mask as 2D (height, width) or 3D (height, width, channel) array + cropping_range : sequence of int + (x0, x1, y0, y1) slices segmentation masks by x0:x1 (horizontally) and y0:y1 (vertically). + If the cropping range exceeds the input image, i.e., x0 < 0, y0 < 0, x1 > width, + or y1 > height, padding is applied. + dtype : str or dtype, optional + Data type of output array. By default, the data type of the input array is preserved. + + Returns + ------- + img_cropped : np.ndarray + Cropped image/mask + """ + if not dtype: + dtype = img.dtype + + height, width = img.shape[:2] + x0, x1, y0, y1 = cropping_range + + shape_cropped = list(img.shape) + shape_cropped[:2] = (y1-y0, x1-x0) + img_cropped = np.zeros(shape_cropped, dtype=dtype) + + offset_x = -min(0, x0) + offset_y = -min(0, y0) + + # get valid coordinates for the original image + x0, x1 = np.clip((x0, x1), 0, width) + y0, y1 = np.clip((y0, y1), 0, height) + + img_cropped[offset_y:offset_y+y1-y0, offset_x:offset_x+x1-x0] = img[y0:y1, x0:x1] + + return img_cropped + + +# pylint: disable=C0103 +def crop_annotations( + annotations: List, + img_width: int, + img_height: int, + cropping_range: Sequence[int], + start_id: int, + task: str, + keep_incomplete: bool = True +) -> Tuple: + """Crop object annotations. + + This function transforms a list of object annotations in COCO format, such that the resulting + annotations refer to a cropped version of the original image. The output only contains objects + with valid, non-empty bounding boxes or segmentations, depending on the specified task. + + Parameters + ---------- + annotations : list of dict + List of object annotations referring to the same image. + img_width : int + (Original) image width + img_height : int + (Original) image height + cropping_range: sequence of int + (x0, x1, y0, y1) slices segmentation masks by x0:x1 (horizontally) and y0:y1 (vertically). + The cropping range may exceed the input image, e.g., through negative start indices. + In this case, padding is assumed. + start_id : int + Object ID to start output annotations with. If None, the original object IDs are preserved. + task : str + Either "bbox-detection" or "instance-segmentation" + keep_incomplete : bool + If false, trimmed objects are discarded. + + + Returns + ------- + next_id + This may be useful as start id for further objects. If start_id=None, None is returned. + annotation_cropped + List of transformed COCO annotations with non-empty segmentation + + Raises + ------ + TypeError + Raised for unsupported object annotations. + ValueError + Raised if input annotations refer to different images. + """ + x_start, x_end, y_start, y_end = cropping_range + annotations_cropped = [] + img_id = None + i_ann = start_id + + for annotation in annotations: + # check image id + if img_id: + if annotation["image_id"] != img_id: + raise ValueError( + "All annotations must refer to the same image, i.e., have equal image_id's." + ) + else: + img_id = annotation["image_id"] + + if task == "bbox-detection": + # get bounding box + bbox_orig = annotation.get("bbox") + if bbox_orig: + if not isinstance(bbox_orig, list) or len(bbox_orig) != 4: + raise TypeError( + "Unknown bbox format, list of length 4 expected." + ) + else: + # skip instance + continue + + # transform box + x1, y1, w, h = bbox_orig + x2, y2 = x1 + w, y1 + h + x1, y1, x2, y2 = (round(coord) for coord in (x1, y1, x2, y2)) + X1 = np.clip(x1 - x_start, 0, x_end - x_start).tolist() + X2 = np.clip(x2 - x_start, 0, x_end - x_start).tolist() + Y1 = np.clip(y1 - y_start, 0, y_end - y_start).tolist() + Y2 = np.clip(y2 - y_start, 0, y_end - y_start).tolist() + area = (X2 - X1) * (Y2 - Y1) + + if area > 0: + if keep_incomplete or (X2-X1 >= x2-x1 and Y2-Y1 >= y2-y1): + # create object annotation + annotations_cropped.append({ + "area": area, + "bbox": [X1, Y1, X2-X1, Y2-Y1], + "image_id": img_id, + "id": i_ann if start_id is not None else annotation["id"], + "category_id": annotation.get("category_id") + }) + if start_id is not None: + i_ann += 1 + + elif task == "instance-segmentation": + # read segmentation + seg_orig = annotation.get("segmentation") + if seg_orig: + mask_orig = coco_seg_to_mask(seg_orig, img_width, img_height) + else: + # skip instances without segmentation + continue + + # crop segmentation + mask_cropped = crop_img_padded(mask_orig, cropping_range) + seg_cropped = imantics.Mask(mask_cropped).polygons().segmentation + + if seg_cropped: + # compare object boundaries + bbox_orig = bbox_from_mask(mask_orig, fmt="xywh").tolist() + bbox_cropped = bbox_from_mask(mask_cropped, fmt="xywh").tolist() + + if ( + keep_incomplete + or (bbox_cropped[2] >= bbox_orig[2] and bbox_cropped[3] >= bbox_orig[3]) + ): + # create object annotation + annotations_cropped.append({ + "area": bbox_cropped[2] * bbox_cropped[3], + "bbox": bbox_cropped, + "segmentation": seg_cropped, + "iscrowd": 0, + "image_id": img_id, + "id": i_ann if start_id is not None else annotation["id"], + "category_id": annotation.get("category_id") + }) + if start_id is not None: + i_ann += 1 + + return i_ann, annotations_cropped + +# pylint: disable=C0103 +def crop_seg_from_coco( + ann_file: str, + img_dir: str, + outdir: str, + padding: int = 0 +): + """ + This function reads annotations in COCO format and crops each segmentation instance from + the corresponding image file. In addition, a new COCO json file is written, which annotates + the cropped images. + + Parameters + ---------- + ann_file : str + COCO json file + img_dir : str + Directory containing JPG images + outdir : str + Directory to which the output is written + padding : int + This option allows to increase the cropping range beyond the borders of a segmented object. + If possible, each side of the corresponding bounding box is shifted by the same number of + pixels. + + Raises + ------ + TypeError + Raised for unsupported segmentation format. + """ + + os.makedirs(os.path.join(outdir, "images"), exist_ok=True) + for path in glob.iglob(os.path.join(outdir, "images", "*")): + os.remove(path) + + info = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + + # image id -> COCO dict of uncropped image + dict_images = dict() + # count cropped objects for each image + obj_counter = defaultdict(int) + + annotations = [] + images = [] + + with open(ann_file, "rb") as f: + ann = json.load(f) + + categories = ann.get("categories") + licenses = ann.get("licenses") + + for image in ann.get("images"): + dict_images[image["id"]] = image + + i_ann = 1 + for annotation in ann.get("annotations"): + img_coco = dict_images[annotation["image_id"]] + + # read image + img_name = os.path.split(img_coco["file_name"])[1] + image = cv2.imread(os.path.join(img_dir, img_name)) + # original size + height = image.shape[0] + width = image.shape[1] + + seg = annotation.get("segmentation") + if seg: + if isinstance(seg, dict): + # rle to mask + seg_mask = mask.decode(seg).astype("bool") + elif isinstance(seg, list): + # polygon to mask + polygons = imantics.Polygons(seg) + seg_mask = polygons.mask(width, height).array + else: + raise TypeError( + "Unknown segmentation format, polygons or RLE expected" + ) + else: + # skip instances without segmentation + continue + + # calculate bounding box from segmentation + bbox = bbox_from_mask(seg_mask, fmt="xyxy").tolist() + if bbox[2] - bbox[0] < 1 or bbox[3] - bbox[1] < 1: + continue + + # apply padding, clip values + x1, y1, x2, y2 = (round(coord) for coord in bbox) + x1, x2 = np.clip((x1 - padding, x2 + padding), 0, width).tolist() + y1, y2 = np.clip((y1 - padding, y2 + padding), 0, height).tolist() + + # crop image + image_cropped = image[y1:y2, x1:x2] + if image_cropped.size == 0: + continue + + img_name_new = "{}_{}.jpg".format( + os.path.splitext(img_name)[0], + obj_counter[annotation["image_id"]] + ) + + outpath = os.path.join( + outdir, + "images", + img_name_new + ) + cv2.imwrite(outpath, image_cropped) + + images.append({ + "id": i_ann, + "file_name": img_name_new, + "height": y2 - y1, + "width": x2 - x1, + "license": img_coco.get("license") + }) + + # annotate cropped instance + mask_cropped = seg_mask[y1:y2, x1:x2] + polygons_cropped = imantics.Mask(mask_cropped).polygons().segmentation + # remove polygons with less than 3 points + polygons_cropped = [p for p in polygons_cropped if len(p) >= 6] + bbox_coco = bbox_from_polygons(polygons_cropped, fmt="xywh").tolist() + + annotations.append({ + "area": bbox_coco[2] * bbox_coco[3], + "bbox": bbox_coco, + "segmentation": polygons_cropped, + "iscrowd": 0, + "image_id": i_ann, + "id": i_ann, + "category_id": annotation["category_id"] + }) + + obj_counter[annotation["image_id"]] += 1 + i_ann += 1 + + # write COCO json file + json_new = os.path.join(outdir, "annotations.json") + with open(json_new, 'w') as json_file: + json.dump({ + 'info': info, + 'licenses': licenses, + 'images': images, + 'annotations': annotations, + 'categories': categories + }, + json_file, + indent = 2, + sort_keys = True + ) + +def crop_bbox_from_coco( + ann_file: str, + img_dir: str, + outdir: str, + padding: int = 0 +): + """ + This function reads annotations in COCO format and crops each contained bounding box from + the corresponding image file. In addition, a new COCO json file is written, which annotates + the cropped images. If no padding is applied, the output boxes cover the complete cropped + images. + + Parameters + ---------- + ann_file : str + COCO json file + img_dir : str + Directory containing JPG images + outdir : str + Directory to which the output is written + padding : int + This option allows to increase the cropping range beyond the borders of the original + bounding box. If possible, each side of the latter is shifted by the same number of + pixels. + + Raises + ------ + TypeError + Raised for unsupported segmentation format. + """ + from .sliding_window_merging import xywh_to_xyxy + + os.makedirs(os.path.join(outdir, "images"), exist_ok=True) + for path in glob.iglob(os.path.join(outdir, "images", "*")): + os.remove(path) + + info = { + "contributor" : "", + "date_created" : datetime.datetime.now().strftime("%Y/%m/%d"), + "description" : "", + "version" : "", + "url" : "", + "year" : "" + } + + # image id -> COCO dict of uncropped image + dict_images = dict() + # count cropped objects for each image + obj_counter = defaultdict(int) + + annotations = [] + images = [] + + with open(ann_file, "rb") as f: + ann = json.load(f) + + categories = ann.get("categories") + licenses = ann.get("licenses") + + for image in ann.get("images"): + dict_images[image["id"]] = image + + i_ann = 1 + for annotation in ann.get("annotations"): + img_coco = dict_images[annotation["image_id"]] + + # read image + img_name = os.path.split(img_coco["file_name"])[1] + image = cv2.imread(os.path.join(img_dir, img_name)) + # original size + height = image.shape[0] + width = image.shape[1] + + bbox = annotation.get("bbox") + if not bbox: + continue + + # apply padding, clip values + x1, y1, x2, y2 = (round(coord) for coord in xywh_to_xyxy(bbox)) + x1, x2 = np.clip((x1 - padding, x2 + padding), 0, width).tolist() + y1, y2 = np.clip((y1 - padding, y2 + padding), 0, height).tolist() + + # crop image + image_cropped = image[y1:y2, x1:x2] + if image_cropped.size == 0: + continue + + img_name_new = "{}_{}.jpg".format( + os.path.splitext(img_name)[0], + obj_counter[annotation["image_id"]] + ) + + outpath = os.path.join( + outdir, + "images", + img_name_new + ) + cv2.imwrite(outpath, image_cropped) + + images.append({ + "id": i_ann, + "file_name": img_name_new, + "height": y2 - y1, + "width": x2 - x1, + "license": img_coco.get("license") + }) + + # map bbox to new coordinate system + bbox_cropped = [bbox[0] - x1, bbox[1] - y1, bbox[2], bbox[3]] + + annotations.append({ + "area": bbox_cropped[2] * bbox_cropped[3], + "bbox": bbox_cropped, + "image_id": i_ann, + "id": i_ann, + "category_id": annotation["category_id"] + }) + + obj_counter[annotation["image_id"]] += 1 + i_ann += 1 + + # write COCO json file + json_new = os.path.join(outdir, "annotations.json") + with open(json_new, 'w') as json_file: + json.dump({ + 'info': info, + 'licenses': licenses, + 'images': images, + 'annotations': annotations, + 'categories': categories + }, + json_file, + indent = 2, + sort_keys = True + ) + +def sliding_window_grid_2d( + img_width: int, + img_height: int, + win_width: int, + win_height: int, + hor_overlap: int, + vert_overlap: int +): + '''sliding_window_grid_2d + + >EXPERIMENTAL< Generate sliding window start and stop indices. + + Parameters + ---------- + img_width: int + Image width (px) + img_height: int + Image height (px) + win_width: int + Window width (px) + win_height: int + Window height (px) + hor_overlap: int + Horizontal overlap (px) + vert_overlap: int + Vertical overlap (px) + + Returns + ------- + np.ndarray + 2D numpy array, where each row consists of the four values + start_x, stop_x, start_y, stop_y. + ''' + xxyy = np.array( + [(*x01, *y01) + for x01 in sw_coords_1d(img_width, win_width, hor_overlap) + for y01 in sw_coords_1d(img_height, win_height, vert_overlap)] + ) + return xxyy + +def crop_ann_img( + img: np.ndarray, + img_ann: dict, + obj_anns: dict, + xxyy: np.ndarray, + obj_id: int, + img_id: int, + task: str = 'instance-segmentation', + return_empty: bool = True, + keep_incomplete: bool = True, +) -> Tuple: + '''crop_ann_img + + Crop sub-images and sub-annotations from an annotated image. + + Parameters + ---------- + img : np.ndarray + Image as numpy array. + img_ann : dict + Image annotation as COCO dict. + obj_anns : dict + Object annotations as list of COCO dicts. + xxyy : np.ndarray + 2D numpy array, where each row consists of the four values + x0, x1, y0, y1 for cropping. + obj_id : int + Start object ID for new, cropped COCO object annotations. + img_id : int + Start object ID for new, cropped COCO images. + task : str, optional + Either 'instance-segmentation' or 'bbox-detection',by default 'instance-segmentation'. + return_empty : bool, optional + Whether images without annotation should be returned, by default True + keep_incomplete : bool + If false, trimmed object annotations are discarded. + + Yields + ------- + Tuple + Tuple of + (cropped_img, cropped_img_ann, cropped_img_name, cropped_obj_anns, img_id, obj_id). + ''' + for cropping_range in xxyy: + # print('cropping_range:', cropping_range) + # print('w, h:', img.shape[1], img.shape[0]) + # print('obj_anns:', obj_anns) + obj_id, cropped_obj_anns = crop_annotations( + annotations=obj_anns, + img_width=img.shape[1], + img_height=img.shape[0], + cropping_range=list(cropping_range), + start_id=obj_id, + task=task, + keep_incomplete=keep_incomplete, + ) + # print('cropped_obj_anns', cropped_obj_anns) + # print() + if not return_empty: + if len(cropped_obj_anns) < 1: + continue + + cropped_img = crop_img_padded(img, cropping_range) + + for ann in cropped_obj_anns: + ann['image_id'] = img_id + + img_name = os.path.basename(img_ann['file_name']).split('.')[0] + # think about whether the name should contain the upper range + # inclusively or exclusively + #cropped_img_name = '{}_{}-{}_{}-{}.jpg'.format(img_name, *cropping_range) + cropped_img_name = '{}_{}x{}_{}-{}_{}-{}.jpg'.format( + img_name, + img.shape[1], + img.shape[0], + *cropping_range + ) + cropped_img_ann = coco_utils.build_coco_image( + image_id = img_id, + file_name = cropped_img_name, + width = cropped_img.shape[1], + height = cropped_img.shape[0], + license = img_ann.get('license', 0), + coco_url = img_ann.get('coco_url', ''), + date_captured = img_ann.get('date_captured', 0), + flickr_url = img_ann.get('flickr_url', ''), + ) + + img_id = img_id + 1 + yield ( + cropped_img, + cropped_img_ann, + cropped_img_name, + cropped_obj_anns, + img_id, + obj_id + ) + +def sliding_window_crop_coco( + img_dir: str, + ann_path: str, + img_dir_out: str, + ann_path_out: str, + win_width: int, + win_height: int, + hor_overlap: int, + vert_overlap: int, + img_id: int = 0, + obj_id: int = 0, + save_empty: bool=True, + keep_incomplete: bool=True, + task: str='instance-segmentation', +): + '''sliding_window_crop_coco + + >Experimental< Crop sliding window subimages and corresponding + annotations from COCO annotated images. + + Parameters + ---------- + img_dir : str + Image directory. + ann_path : str + COCO annotation path. + img_dir_out : str + Output directory for images. + ann_path_out : str + Output path for COCO annotation. + win_width: int + Window width (px) + win_height: int + Window height (px) + hor_overlap: int + Horizontal overlap of neighboring windows (px) + vert_overlap: int + Vertical overlap of neighboring windows (px) + img_id : int, optional + Start image ID for new COCO images, by default 0 + obj_id : int, optional + Start image ID for new COCO object annotations, by default 0 + save_empty : bool, optional + Whether images without annotations should be saved, by default True + keep_incomplete : bool, optional + If false, trimmed object annotations are discarded. + task : str, optional + Task the dataset will be used for. Eiter "bbox-detection" or + "instance-segmentation" + ''' + ann = load_coco_ann(ann_path) + img_anns = ann['images'] + + new_obj_anns = [] + new_img_anns = [] + + for img_ann in img_anns: + img = cv2.imread(os.path.join(img_dir, img_ann['file_name'])) + obj_anns = get_obj_anns(img_ann, ann) + + xxyy = sliding_window_grid_2d( + img.shape[1], + img.shape[0], + win_width, + win_height, + hor_overlap, + vert_overlap + ) + + i_id, o_id = img_id, obj_id + for c_img, c_img_ann, c_img_name, c_obj_anns, i_id, o_id in crop_ann_img( + img=img, + img_ann=img_ann, + obj_anns=obj_anns, + xxyy=xxyy, + obj_id=obj_id, + img_id=img_id, + return_empty=save_empty, + keep_incomplete=keep_incomplete, + task=task + ): + new_img_anns.append(c_img_ann) + new_obj_anns.extend(c_obj_anns) + + cv2.imwrite( + os.path.join(img_dir_out, c_img_name), + c_img, + ) + + img_id, obj_id = i_id, o_id + + # print('new_obj_anns:', new_obj_anns) + + new_ann = coco_utils.build_coco_dataset( + annotations = new_obj_anns, + images = new_img_anns, + categories = ann.get('categories'), + licenses = ann.get('licenses'), + info = ann.get('info') + ) + + with open(ann_path_out, 'w') as ann_f: + json.dump(new_ann, ann_f) + +def crop_pvoc_obj( + obj: xml.etree.ElementTree.ElementTree, + cropping_range: Sequence[float], + min_size: Sequence[float] = [10, 10], + keep_incomplete: bool = True, +) -> Optional[xml.etree.ElementTree.ElementTree]: + '''crop_pvoc_obj + + Crop PVOC object to specified range. + + Parameters + ---------- + obj : xml.etree.ElementTree.ElementTree + PVOC object as ElementTree + cropping_range : Sequence[float] + Cropping range in x0x1y0y1 format. + min_size : Sequence[float], optional + Minimum cropped bounding-box size (width, height), + by default [10, 10]. + keep_incomplete : bool + If false, trimmed object annotations are discarded. + + Returns + ------- + Optional[xml.etree.ElementTree.ElementTree] + Cropped PVOC object, or None if the cropped object is + smaller than min_size. + ''' + cropped_obj = copy.deepcopy(obj) + + bbox = get_pvoc_obj_bbox(obj) + w_orig, h_orig = bbox_size(bbox) + + bbox_cropped = crop_bbox(bbox, cropping_range) + w, h = bbox_size(bbox_cropped) + + if not keep_incomplete: + if not (w_orig == w and h_orig == h): + return None + + if w < min_size[0] or h < min_size[1]: + return None + + set_pvoc_obj_bbox(cropped_obj, bbox_cropped) + + return cropped_obj + +def crop_pvoc_ann( + ann: xml.etree.ElementTree.ElementTree, + cropping_range: Sequence[float], + min_size: Sequence[float] = [10, 10], + rename: bool = True, + keep_incomplete: bool = True, +) -> xml.etree.ElementTree.ElementTree: + '''crop_pvoc_ann + + Crop PVOC annotation to specified range. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + cropping_range : Sequence[float] + Cropping range in x0x1y0y1 format. + min_size : Sequence[float], optional + Minimum cropped bounding-box size (width, height), + by default [10, 10]. + rename : bool, optional + Whether the filename element text should be change to include + the cropping coordinates, by default True + keep_incomplete : bool + If false, trimmed object annotations are discarded. + + Returns + ------- + xml.etree.ElementTree.ElementTree + Cropped PVOC annotation as ElementTree + ''' + cropped_ann = copy.deepcopy(ann) + drop_pvoc_objects(cropped_ann) + + if rename: + nm, ext = os.path.splitext( + os.path.basename(get_pvoc_filename(ann)) + ) + xmn, xmx, ymn, ymx = cropping_range + img_width, img_height, _ = get_pvoc_size(ann) + new_name = f'{nm}_{img_width}x{img_height}_{xmn}-{xmx}_{ymn}-{ymx}{ext}' + set_pvoc_filename(cropped_ann, new_name) + + cropped_objs = [ + obj for obj in [ + crop_pvoc_obj( + obj, cropping_range, min_size, + keep_incomplete=keep_incomplete, + ) for obj in get_pvoc_objects(ann) + ] if not obj is None + ] + + for obj in cropped_objs: + add_pvoc_object(cropped_ann, obj) + set_pvoc_size( + cropped_ann, + [ + cropping_range[1] - cropping_range[0], + cropping_range[3] - cropping_range[2], + get_pvoc_size(ann)[2], + ] + ) + + return cropped_ann + +def crop_image( + img: np.ndarray, + cropping_range: Sequence[int], +) -> np.ndarray: + '''crop_image + + Crop image to specified range. + + Parameters + ---------- + img : np.ndarray + Image as numpy array. + cropping_range : Sequence[int] + Cropping range in x0x1y0y1 format. + + Returns + ------- + np.ndarray + Cropped image as numpy array. + ''' + return img[ + cropping_range[2]:cropping_range[3], + cropping_range[0]:cropping_range[1], + ] + +def sliding_window_crop_pvoc( + img_dir: str, + ann_dir: str, + img_dir_out: str, + ann_dir_out: str, + win_width: int, + win_height: int, + hor_overlap: int, + vert_overlap: int, + save_empty: bool = True, + keep_incomplete: bool = True, +): + '''sliding_window_crop_pvoc + + Sliding-window crop images and annotation from + PVOC annotated images. + + Parameters + ---------- + img_dir : str + Path to image directory + ann_dir : str + Path to annotation directory + img_dir_out : str + Path to output image directory + ann_dir_out : str + Path to output annotation directory + win_width: int + Window width (px) + win_height: int + Window height (px) + hor_overlap: int + Horizontal overlap of neighboring windows (px) + vert_overlap: int + Vertical overlap of neighboring windows (px) + save_empty : bool, optional + Whether cropped images without object annotations should be saved, + by default True + keep_incomplete : bool + If false, trimmed object annotations are discarded. + ''' + ann_files = glob.glob(os.path.join(ann_dir, '*.xml')) + + for ann_f in ann_files: + ann = load_pvoc_annotation(ann_f) + img = cv2.imread(os.path.join(img_dir, get_pvoc_filename(ann))) + + xxyy = sliding_window_grid_2d( + img.shape[1], + img.shape[0], + win_width, + win_height, + hor_overlap, + vert_overlap + ).astype(int) + + # print(get_pvoc_filename(ann)) + + for cropping_range in xxyy: + cropped_ann = crop_pvoc_ann( + ann, cropping_range, + keep_incomplete=keep_incomplete + ) + + if not save_empty and len(get_pvoc_objects(cropped_ann)) < 1: + continue + + cropped_img = crop_img_padded(img, cropping_range) + + filename, ext = os.path.splitext(get_pvoc_filename(cropped_ann)) + ann_filepath = os.path.join(ann_dir_out, f'{filename}.xml') + write_pvoc_annotation( + cropped_ann, + ann_filepath + ) + img_filepath = os.path.join(img_dir_out, f'{filename}{ext}') + cv2.imwrite(img_filepath, cropped_img) + +def sliding_window_crop_images( + img_dir: str, + img_dir_out: str, + win_width: int, + win_height: int, + hor_overlap: int, + vert_overlap: int, +): + '''sliding_window_crop_images + + Sliding-window crop images. + + Parameters + ---------- + img_dir : str + Path to image directory + img_dir_out : str + Path to output image directory + win_width: int + Window width (px) + win_height: int + Window height (px) + hor_overlap: int + Horizontal overlap of neighboring windows (px) + vert_overlap: int + Vertical overlap of neighboring windows (px) + ''' + from .utils import get_image_files + img_files = get_image_files(img_dir=img_dir) + + for img_f in img_files: + img = cv2.imread(img_f) + + xxyy = sliding_window_grid_2d( + img.shape[1], + img.shape[0], + win_width, + win_height, + hor_overlap, + vert_overlap + ).astype(int) + + # print(get_pvoc_filename(ann)) + + img_name, ext = os.path.splitext(os.path.basename(img_f)) + img_width = img.shape[1] + img_height = img.shape[0] + + for cropping_range in xxyy: + cropped_img = crop_img_padded(img, cropping_range) + + xmn, xmx, ymn, ymx = cropping_range + + cropped_img_name = f'{img_name}_{img_width}x{img_height}_{xmn}-{xmx}_{ymn}-{ymx}{ext}' + + img_filepath = os.path.join(img_dir_out, f'{cropped_img_name}') + cv2.imwrite(img_filepath, cropped_img) diff --git a/ginjinn/utils/sliding_window_merging.py b/ginjinn/utils/sliding_window_merging.py new file mode 100644 index 0000000..6551ab9 --- /dev/null +++ b/ginjinn/utils/sliding_window_merging.py @@ -0,0 +1,1001 @@ +'''Module for merging sliding-window cropped datasets +''' + +import os +import copy +import shutil +import json +import itertools +from collections import defaultdict +from operator import itemgetter +from tempfile import TemporaryDirectory +from typing import Iterable, List, Tuple, Callable +import cv2 +import imantics +import numpy as np +from scipy.sparse.csgraph import connected_components +from ginjinn.simulation import coco_utils +from .dataset_cropping import crop_annotations, crop_image +from .utils import load_coco_ann, get_obj_anns, coco_seg_to_mask, bbox_from_mask + +# source: https://stackoverflow.com/a/52604722 +class NumpyEncoder(json.JSONEncoder): + """ Special json encoder for numpy types """ + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + return json.JSONEncoder.default(self, obj) + +def get_bname_from_fname(file_name: str) -> str: + '''get_bname_from_fname + + Get original image name from sliding-window cropped file name. + + Parameters + ---------- + file_name : str + sliding-window cropped image file name + + Returns + ------- + str + original image name sans extension + ''' + fname, _ = os.path.splitext(file_name) + fname_split = fname.split('_') + return '_'.join(fname_split[:-3]) + +def get_coords_from_fname(file_name: str) -> np.ndarray: + '''get_coords_from_fname + + Get bounding-box coordinates on the original image of a sliding-window + crop from file name. + + Parameters + ---------- + file_name : str + sliding-window cropped image file name + + Returns + ------- + np.ndarray + numpy array of bounding box coordinates on original image (x0, x1, y0, y1). + ''' + fname, _ = os.path.splitext(file_name) + fname_split = fname.split('_') + y0, y1 = [int(coord) for coord in fname_split[-1].rsplit('-', 1)] + x0, x1 = [int(coord) for coord in fname_split[-2].rsplit('-', 1)] + + return np.array([x0, x1, y0, y1]) + +def get_size_from_fname(file_name: str) -> np.ndarray: + '''get_size_from_fname + + Get size of the original image from the file name of a sliding-window crop. + + Parameters + ---------- + file_name : str + Sliding-window cropped image file name + + Returns + ------- + np.ndarray + Image size of original image (width, height) + ''' + fname, _ = os.path.splitext(file_name) + fname_split = fname.split('_') + width, height = [int(x) for x in fname_split[-3].split('x')] + + return np.array([width, height]) + +def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray: + '''xywh_to_xyxy + + Translate bounding box format from x0y0wh to x0y0x1y1 + + Parameters + ---------- + xywh : np.ndarray + bbox in x0y0wh format + + Returns + ------- + np.ndarray + bbox in x0y0x1y1 format + ''' + xyxy = np.array(xywh) + xyxy[2:4] = xyxy[0:2] + xyxy[2:4] + return xyxy + +def xyxy_to_xywh(xyxy: np.ndarray) -> np.ndarray: + '''xywh_to_xyxy + + Translate bounding box format from x0y0x1y1 to x0y0wh + + Parameters + ---------- + xyxy : np.ndarray + bbox in x0y0x1y1 format + + Returns + ------- + np.ndarray + bbox in x0y0wh format + ''' + xywh = np.array(xyxy) + xywh[2:4] = xywh[2:4] - xywh[0:2] + return xywh + +def intersection_bboxes( + a: np.ndarray, + b: np.ndarray, + intersection_type: str='iou' +) -> float: + '''intersection_bboxes + + Calculate intersection of two bounding-boxes a and b. + + Parameters + ---------- + a : np.ndarray + Bounding-box in x0y0x1y1 format. + b : np.ndarray + Bounding-box in x0y0x1y1 format. + intersection_type : str, optional + Type or intersection to calculate, by default 'iou'. + + Possible types are: + - "simple": absolute intersection area + - "iou": intersection over union (intersection area / union area) + - "ios": intersection over smaller (intersection area / smaller bbox area) + + Returns + ------- + float + Intersection + + Raises + ------ + Exception + Raised when an invalid intersection type is passed. + ''' + dx = min(a[2], b[2]) - max(a[0], b[0]) + dy = min(a[3], b[3]) - max(a[1], b[1]) + + if (dx>=0) and (dy>=0): + intersection = dx*dy + if intersection_type == 'simple': + return intersection + elif intersection_type == 'iou': + w = max(a[2], b[2]) - min(a[0], b[0]) + h = max(a[3], b[3]) - min(a[1], b[1]) + + return intersection / (w * h) + elif intersection_type == 'ios': + w = min(a[2]-a[0], b[2]-b[0]) + h = min(a[3]-a[1], b[3]-b[1]) + + return intersection / (w * h) + else: + msg = f'Invalid intersection_type argument "{intersection_type}". ' +\ + 'Available arguments are "simple", "iou", "ios".' + raise Exception(msg) + + return 0.0 + +def intersection_bboxes_coco( + a: np.ndarray, + b: np.ndarray, + intersection_type: str='iou', +): + '''intersection_bboxes_coco + + Calculate intersection of two COCO bounding-boxes a and b. + + Parameters + ---------- + a : np.ndarray + Bounding-box in x0y0wh format. + b : np.ndarray + Bounding-box in x0y0wh format. + intersection_type : str, optional + Type or intersection to calculate, by default 'iou'. + + Possible types are: + - "simple": absolute intersection area + - "iou": intersection over union (intersection area / union area) + - "ios": intersection over smaller (intersection area / smaller bbox area) + + Returns + ------- + float + Intersection + ''' + # a = [a[0], a[1], a[0]+a[2], a[1]+a[3]] + # b = [b[0], b[1], b[0]+b[2], b[1]+b[3]] + a = xywh_to_xyxy(a) + b = xywh_to_xyxy(b) + + return intersection_bboxes(a, b, intersection_type=intersection_type) + +def calc_intersection_matrix( + bboxes: np.ndarray, + intersection_type: str='iou' +) -> np.ndarray: + '''calc_intersection_matrix + + Calculate pair-wise intersection matrix for bounding-boxes. + + Parameters + ---------- + bboxes : np.ndarray + n * 4 np.array of bounding boxes in x0y0x1y1 format. + Each row represents a single bounding box. + intersection_type : str, optional + Type or intersection to calculate, by default 'iou'. + + Possible types are: + - "simple": absolute intersection area + - "iou": intersection over union (intersection area / union area) + - "ios": intersection over smaller (intersection area / smaller bbox area) + + Returns + ------- + np.ndarray + n * n matrix of pairwise intersections. + ''' + intersection_matrix = np.ones((bboxes.shape[0], bboxes.shape[0])) + for i in range(0, bboxes.shape[0]-1): + for j in range(i + 1, bboxes.shape[0]): + intersection_matrix[i, j] = intersection_bboxes( + bboxes[i], bboxes[j], + intersection_type=intersection_type + ) + intersection_matrix[j, i] = intersection_matrix[i, j] + + return intersection_matrix + +def reconstruct_original_image( + img_anns: List[dict], + img_dir: str, +) -> np.ndarray: + '''reconstruct_original_image + + Reconstruct the original image from cropped sub images. + + Parameters + ---------- + img_anns : List[dict] + List of COCO image annotations as dictionary. + img_dir : str + Directory containing the images corresponding to img_anns. + + Returns + ------- + np.ndarray + RGB image as numpy array (h * w * 3) + ''' + orig_w, orig_h = get_size_from_fname(img_anns[0]['file_name']) + orig_img = np.zeros((orig_h, orig_w, 3), dtype="uint8") + + for img_ann in img_anns: + sub_img = cv2.imread(os.path.join(img_dir, img_ann['file_name'])) + + xxyy = get_coords_from_fname(img_ann['file_name']) + orig_img[xxyy[2]:xxyy[3], xxyy[0]:xxyy[1], :] = sub_img + + return orig_img + +def merge_segmentations( + img_anns: List[dict], + obj_anns: List[dict] +): + '''merge_segmentations + + Merge objects from sliding-window predictions such that the output annotations refer + to the original image. + + Parameters + ---------- + img_anns : list of dict + List of COCO image annotations as dictionary. + obj_anns : list of dict + List of COCO object annotations as dictionary. + + Returns + ------- + polygons : list of list of float + Segmentation of merged object + bbox : np.ndarray + Corresponding bounding box (COCO format) + ''' + dict_images = {ann["id"]: ann for ann in img_anns} + + orig_w, orig_h = get_size_from_fname(img_anns[0]['file_name']) + mask = np.zeros((orig_h, orig_w), dtype=np.bool_) + + for obj_ann in obj_anns: + img_ann = dict_images[obj_ann["image_id"]] + sub_mask = coco_seg_to_mask( + obj_ann["segmentation"], + img_ann["width"], + img_ann["height"] + ) + xxyy = get_coords_from_fname(img_ann["file_name"]) + mask[xxyy[2]:xxyy[3], xxyy[0]:xxyy[1]] = np.logical_or( + mask[xxyy[2]:xxyy[3], xxyy[0]:xxyy[1]], + sub_mask + ) + + polygons = imantics.Mask(mask).polygons().segmentation + polygons = [p for p in polygons if len(p) >= 6] + bbox = bbox_from_mask(mask, fmt="xywh") + return polygons, bbox + +def reconstruct_annotations_on_original( + img_anns: List[dict], + obj_anns: List[dict], +) -> List[dict]: + '''reconstruct_annotations_on_original + + Reconstruct object annotations in the coordinate system of + the original, sliding-window croppped image. + + Parameters + ---------- + img_anns : List[dict] + List of COCO image annotations as dictionary. + obj_anns : List[dict] + List of COCO object annotations as dictionary. + + Returns + ------- + List[dict] + List of COCO image annotations in the coordinate system of + the original image as dictionary. + ''' + orig_obj_anns = [] + for img_ann in img_anns: + xxyy = get_coords_from_fname(img_ann['file_name']) + + sub_obj_anns = [obj_ann for obj_ann in obj_anns if obj_ann['image_id'] == img_ann['id']] + for obj_ann in sub_obj_anns: + orig_obj_ann = copy.deepcopy(obj_ann) + orig_obj_ann['bbox'][0] = obj_ann['bbox'][0] + xxyy[0] + orig_obj_ann['bbox'][1] = obj_ann['bbox'][1] + xxyy[2] + orig_obj_ann['image_id'] = img_anns[0]['id'] + + orig_obj_anns.append(orig_obj_ann) + + return orig_obj_anns + +def merge_bbox_annotations( + obj_anns: List[dict], + img_id: int, + intersection_type: str='iou', + intersection_th: float=0.6, +) -> List[dict]: + '''merge_bbox_annotations + + Merge bounding-box annotations using single-linkage hierarchical + clustering based on pairwise intersections. + + Parameters + ---------- + obj_anns : List[dict] + List of COCO object annotations as dictionary. + img_id : int + Image ID merged object annotations should refer to. + intersection_type : str, optional + Type or intersection to calculate, by default 'iou'. + + Possible types are: + - "simple": absolute intersection area + - "iou": intersection over union (intersection area / union area) + - "ios": intersection over smaller (intersection area / smaller bbox area) + intersection_th : float, optional + Intersection threshold for the clustering cut-off, by default 0.6 + + Returns + ------- + List[dict] + List of COCO object annotations as dictionary. + ''' + from sklearn.cluster import AgglomerativeClustering + + if len(obj_anns) < 1: + return [] + + bboxes = xywh_to_xyxy([o_ann['bbox'] for o_ann in obj_anns]) + intersection_matrix = calc_intersection_matrix(bboxes, intersection_type=intersection_type) + if intersection_matrix.shape[0] < 2: + cl = np.array([0], dtype=int) + else: + ac = AgglomerativeClustering( + n_clusters=None, + distance_threshold=1-intersection_th, + affinity='precomputed', + linkage='single' + ) + cl = ac.fit_predict(1 - intersection_matrix) + + new_anns = [] + for cl_id in np.unique(cl): + cl_idcs = np.argwhere(cl == cl_id).flatten() + bboxes_cl = bboxes[cl == cl_id] + bbox_merged = np.array([*bboxes_cl.min(0)[:2], *bboxes_cl.max(0)[2:]]) + + new_ann = copy.deepcopy(obj_anns[cl_idcs[0]]) + bbox_xywh = xyxy_to_xywh(bbox_merged).flatten() + new_ann['bbox'] = list(bbox_xywh) + new_ann['area'] = float(bbox_xywh[2] * bbox_xywh[3]) + new_ann['image_id'] = int(img_id) + new_anns.append(new_ann) + + return new_anns + +# source: itertools recipes +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + +def merge_xyxy(boxes: Iterable): + """merge_xyxy + + Merge bounding boxes in PascalVOC format. + + Parameters + ---------- + boxes : [iterable of] iterable of int or float + Bounding boxes in PascalVOC format, i.e., (x_min, y_min, x_max, y_max) + + Returns + ------- + np.ndarray + Bounding box enclosing the input boxes (PascalVOC format) + """ + if np.isscalar(boxes[0]) and len(boxes) == 4: + return np.array(boxes) + x0 = min(box[0] for box in boxes) + y0 = min(box[1] for box in boxes) + x1 = max(box[2] for box in boxes) + y1 = max(box[3] for box in boxes) + return np.array((x0, y0, x1, y1)) + +def merge_window_predictions_bbox( + img_anns: List[dict], + obj_anns: List[dict], + img_dir: str, + iou_threshold: float = 0, + ios_threshold: float = 0, + intersection_threshold: float = 0 +) -> Tuple[np.ndarray, List[dict], List[dict]]: + """merge_window_predictions_bbox + + Merge bounding boxes from sliding-window cropped COCO annotations. Two objects from different + windows are only merged if their intersection satisfies the specified conditions or if further + objects act as connectors. The three conditions are combined in this way: + ((IoU >= iou_threshold) OR (IoS >= ios_threshold)) AND (intersection >= intersection_threshold) + + Parameters + ---------- + img_anns : list of dict + List of COCO image annotations as dictionary. + obj_anns : list of dict + List of COCO object annotations as dictionary. + img_dir : str + Directory containing the images, img_anns refer to. + iou_threshold : float + Min. intersection over union of two objects to be merged (0 = disabled). + Note that the denominator (union) is only calculated within the overlapping region + of two sliding windows. + ios_threshold : float + Min. intersection over smaller area (0 = disabled). + The latter considers the object within the whole sliding window, not only within + the window overlap. + intersection_threshold : float + Min. absolute intersection. + + Returns + ------- + Tuple[np.ndarray, List[dict], List[dict]] + Tuple containing the reconstructed original image as np.ndarray, + a new COCO annotation dict for the reconstructed original image, + and the merged annotations in the coordinate system of the original image, + i.e.: (orig_img, orig_img_ann, merged_obj_anns) + """ + # get position of each window on the original image + coord_list = [] + for img_ann in img_anns: + x0, x1, y0, y1 = get_coords_from_fname(img_ann["file_name"]).tolist() + coord_list.append((x0, x1, y0, y1, img_ann["id"])) + + obj_map = {ann["id"]: i for i, ann in enumerate(obj_anns)} + adj_mat = np.zeros((len(obj_anns), len(obj_anns)), dtype=np.bool_) + + # find horizontal overlaps + coord_list.sort(key=itemgetter(2, 0)) + for y0, row in itertools.groupby(coord_list, key=itemgetter(2)): + # process pairs of consecutive images + for coords1, coords2 in pairwise(row): + obj_anns_1 = [ann for ann in obj_anns if ann["image_id"] == coords1[4]] + obj_anns_2 = [ann for ann in obj_anns if ann["image_id"] == coords2[4]] + + for ann1 in obj_anns_1: + for ann2 in obj_anns_2: + if ann1["category_id"] != ann2["category_id"]: + continue + + # bboxes -> coordinate system of orig. image + bbox1 = copy.deepcopy(ann1["bbox"]) + orig_size1 = bbox1[2] * bbox1[3] + bbox1[0] += coords1[0] + bbox1[1] += coords1[2] + bbox1 = xywh_to_xyxy(bbox1) + + bbox2 = copy.deepcopy(ann2["bbox"]) + orig_size2 = bbox2[2] * bbox2[3] + bbox2[0] += coords2[0] + bbox2[1] += coords2[2] + bbox2 = xywh_to_xyxy(bbox2) + + # clip x coordinates to window overlap + bbox1[0], bbox1[2] = np.clip((bbox1[0], bbox1[2]), coords2[0], coords1[1]) + bbox2[0], bbox2[2] = np.clip((bbox2[0], bbox2[2]), coords2[0], coords1[1]) + + # evaluate object intersection + if bbox1[2] - bbox1[0] > 0 and bbox2[2] - bbox2[0] > 0: + IoU = intersection_bboxes(bbox1, bbox2, intersection_type="iou") + #IoS = intersection_bboxes(bbox1, bbox2, intersection_type="ios") + IoS = intersection_bboxes(bbox1, bbox2, intersection_type="simple") / min(orig_size1, orig_size2) + intersection = intersection_bboxes(bbox1, bbox2, intersection_type="simple") + + # update adjacency matrix + if ( + ( + (iou_threshold and IoU >= iou_threshold) + or (ios_threshold and IoS >= ios_threshold) + ) + and intersection >= intersection_threshold + ): + obj_ind_1 = obj_map[ann1["id"]] + obj_ind_2 = obj_map[ann2["id"]] + adj_mat[obj_ind_1, obj_ind_2] = 1 + adj_mat[obj_ind_2, obj_ind_1] = 1 # unnecessary (in connected_components(), connection defaults to "weak") + + # find vertical overlaps + coord_list.sort(key=itemgetter(0, 2)) + for x0, col in itertools.groupby(coord_list, key=itemgetter(0)): + # process pairs of consecutive images + for coords1, coords2 in pairwise(col): + obj_anns_1 = [ann for ann in obj_anns if ann["image_id"] == coords1[4]] + obj_anns_2 = [ann for ann in obj_anns if ann["image_id"] == coords2[4]] + + for ann1 in obj_anns_1: + for ann2 in obj_anns_2: + if ann1["category_id"] != ann2["category_id"]: + continue + + # bboxes -> coordinate system of orig. image + bbox1 = copy.deepcopy(ann1["bbox"]) + orig_size1 = bbox1[2] * bbox1[3] + bbox1[0] += coords1[0] + bbox1[1] += coords1[2] + bbox1 = xywh_to_xyxy(bbox1) + + bbox2 = copy.deepcopy(ann2["bbox"]) + orig_size2 = bbox2[2] * bbox2[3] + bbox2[0] += coords2[0] + bbox2[1] += coords2[2] + bbox2 = xywh_to_xyxy(bbox2) + + # clip y coordinates to window overlap + bbox1[1], bbox1[3] = np.clip((bbox1[1], bbox1[3]), coords2[2], coords1[3]) + bbox2[1], bbox2[3] = np.clip((bbox2[1], bbox2[3]), coords2[2], coords1[3]) + + # evaluate object intersection + if bbox1[3] - bbox1[1] > 0 and bbox2[3] - bbox2[1] > 0: + IoU = intersection_bboxes(bbox1, bbox2, intersection_type="iou") + #IoS = intersection_bboxes(bbox1, bbox2, intersection_type="ios") + IoS = intersection_bboxes(bbox1, bbox2, intersection_type="simple") / min(orig_size1, orig_size2) + intersection = intersection_bboxes(bbox1, bbox2, intersection_type="simple") + + # update adjacency matrix + if ( + ( + (iou_threshold and IoU >= iou_threshold) + or (ios_threshold and IoS >= ios_threshold) + ) + and intersection >= intersection_threshold + ): + obj_ind_1 = obj_map[ann1["id"]] + obj_ind_2 = obj_map[ann2["id"]] + adj_mat[obj_ind_1, obj_ind_2] = 1 + adj_mat[obj_ind_2, obj_ind_1] = 1 # unnecessary + + orig_img = reconstruct_original_image(img_anns, img_dir) + orig_img_ann = coco_utils.build_coco_image( + image_id = int(img_anns[0]["id"]), + file_name = get_bname_from_fname(img_anns[0]["file_name"]) + ".jpg", + width = int(orig_img.shape[1]), + height = int(orig_img.shape[0]), + coco_url = str(img_anns[0].get("coco_url", "")), + date_captured = str(img_anns[0].get("date_captured", 0)), + flickr_url = str(img_anns[0].get("flickr_url", "")), + ) + + # identify groups of objects to be merged + n_comp, comp_labels = connected_components(adj_mat, directed=False) + + merged_obj_anns = [] + for i_comp in range(n_comp): + inds_merge = np.where(comp_labels == i_comp)[0].tolist() + obj_anns_merge = reconstruct_annotations_on_original( + img_anns, + [obj_anns[i] for i in inds_merge] + ) + bbox = merge_xyxy([xywh_to_xyxy(ann["bbox"]) for ann in obj_anns_merge]) + bbox = xyxy_to_xywh(bbox) + merged_obj_anns.append({ + "area": bbox[2] * bbox[3], + "bbox": bbox, + "segmentation": [], + "iscrowd": 0, + "image_id": orig_img_ann["id"], + "id": obj_anns[inds_merge[0]]["id"], + "category_id": obj_anns[inds_merge[0]]["category_id"] + }) + + return orig_img, orig_img_ann, merged_obj_anns + +def merge_window_predictions_seg( + img_anns: List[dict], + obj_anns: List[dict], + img_dir: str, + iou_threshold: float = 0, + ios_threshold: float = 0, + intersection_threshold: float = 0 +) -> Tuple[np.ndarray, List[dict], List[dict]]: + """merge_window_predictions_seg + + Merge instance segmentations from sliding-window cropped COCO annotations. Two objects from + different windows are only merged if their intersection satisfies the specified conditions or + if further objects act as connectors. The three conditions are combined in this way: + ((IoU >= iou_threshold) OR (IoS >= ios_threshold)) AND (intersection >= intersection_threshold) + + Parameters + ---------- + img_anns : list of dict + List of COCO image annotations as dictionary. + obj_anns : list of dict + List of COCO object annotations as dictionary. + img_dir : str + Directory containing the images, img_anns refer to. + iou_threshold : float + Min. intersection over union of two objects to be merged (0 = disabled). + Note that the denominator (union) is only calculated within the overlapping region + of two sliding windows. + ios_threshold : float + Min. intersection over smaller area (0 = disabled). + The latter considers the object within the whole sliding window, not only within + the window overlap. + intersection_threshold : float + Min. absolute intersection. + + Returns + ------- + Tuple[np.ndarray, List[dict], List[dict]] + Tuple containing the reconstructed original image as np.ndarray, + a new COCO annotation dict for the reconstructed original image, + and the merged annotations in the coordinate system of the original image, + i.e.: (orig_img, orig_img_ann, merged_obj_anns) + """ + # get position of each window on the original image + coord_list = [] + for img_ann in img_anns: + x0, x1, y0, y1 = get_coords_from_fname(img_ann["file_name"]).tolist() + coord_list.append((x0, x1, y0, y1, img_ann["id"])) + + obj_map = {ann["id"]: i for i, ann in enumerate(obj_anns)} + adj_mat = np.zeros((len(obj_anns), len(obj_anns)), dtype=np.bool_) + + # find horizontal overlaps + coord_list.sort(key=itemgetter(2, 0)) + for y0, row in itertools.groupby(coord_list, key=itemgetter(2)): + # process pairs of consecutive images + for coords1, coords2 in pairwise(row): + obj_anns_1 = [ann for ann in obj_anns if ann["image_id"] == coords1[4]] + obj_anns_2 = [ann for ann in obj_anns if ann["image_id"] == coords2[4]] + + for ann1 in obj_anns_1: + for ann2 in obj_anns_2: + if ann1["category_id"] != ann2["category_id"]: + continue + + mask1 = coco_seg_to_mask( + ann1["segmentation"], + coords1[1] - coords1[0], + coords1[3] - coords1[2] + ) + mask2 = coco_seg_to_mask( + ann2["segmentation"], + coords2[1] - coords2[0], + coords2[3] - coords2[2] + ) + orig_size1 = mask1.sum() + orig_size2 = mask2.sum() + + # extract overlap + mask1 = mask1[:, coords2[0] - coords1[0]:] + mask2 = mask2[:, :coords1[1] - coords2[0]] + + # evaluate object intersection + if mask1.sum() > 0 and mask2.sum() > 0: + IoU = np.logical_and(mask1, mask2).sum() / np.logical_or(mask1, mask2).sum() + #IoS = np.logical_and(mask1, mask2).sum() / min(mask1.sum(), mask2.sum()) + IoS = np.logical_and(mask1, mask2).sum() / min(orig_size1, orig_size2) + intersection = np.logical_and(mask1, mask2).sum() + + # update adjacency matrix + if ( + ( + (iou_threshold and IoU >= iou_threshold) + or (ios_threshold and IoS >= ios_threshold) + ) + and intersection >= intersection_threshold + ): + obj_ind_1 = obj_map[ann1["id"]] + obj_ind_2 = obj_map[ann2["id"]] + adj_mat[obj_ind_1, obj_ind_2] = 1 + adj_mat[obj_ind_2, obj_ind_1] = 1 + + # find vertical overlaps + coord_list.sort(key=itemgetter(0, 2)) + for x0, col in itertools.groupby(coord_list, key=itemgetter(0)): + # process pairs of consecutive images + for coords1, coords2 in pairwise(col): + obj_anns_1 = [ann for ann in obj_anns if ann["image_id"] == coords1[4]] + obj_anns_2 = [ann for ann in obj_anns if ann["image_id"] == coords2[4]] + + for ann1 in obj_anns_1: + for ann2 in obj_anns_2: + if ann1["category_id"] != ann2["category_id"]: + continue + + mask1 = coco_seg_to_mask( + ann1["segmentation"], + coords1[1] - coords1[0], + coords1[3] - coords1[2] + ) + mask2 = coco_seg_to_mask( + ann2["segmentation"], + coords2[1] - coords2[0], + coords2[3] - coords2[2] + ) + orig_size1 = mask1.sum() + orig_size2 = mask2.sum() + + # extract overlap + mask1 = mask1[coords2[2] - coords1[2]:, :] + mask2 = mask2[:coords1[3] - coords2[2], :] + + # evaluate object intersection + if mask1.sum() > 0 and mask2.sum() > 0: + IoU = np.logical_and(mask1, mask2).sum() / np.logical_or(mask1, mask2).sum() + #IoS = np.logical_and(mask1, mask2).sum() / min(mask1.sum(), mask2.sum()) + IoS = np.logical_and(mask1, mask2).sum() / min(orig_size1, orig_size2) + intersection = np.logical_and(mask1, mask2).sum() + + # update adjacency matrix + if ( + ( + (iou_threshold and IoU >= iou_threshold) + or (ios_threshold and IoS >= ios_threshold) + ) + and intersection >= intersection_threshold + ): + obj_ind_1 = obj_map[ann1["id"]] + obj_ind_2 = obj_map[ann2["id"]] + adj_mat[obj_ind_1, obj_ind_2] = 1 + adj_mat[obj_ind_2, obj_ind_1] = 1 # unnecessary + + orig_img = reconstruct_original_image(img_anns, img_dir) + orig_img_ann = coco_utils.build_coco_image( + image_id = int(img_anns[0]["id"]), + file_name = get_bname_from_fname(img_anns[0]["file_name"]) + ".jpg", + width = int(orig_img.shape[1]), + height = int(orig_img.shape[0]), + coco_url = str(img_anns[0].get("coco_url", "")), + date_captured = str(img_anns[0].get("date_captured", 0)), + flickr_url = str(img_anns[0].get("flickr_url", "")), + ) + + # identify groups of objects to be merged + n_comp, comp_labels = connected_components(adj_mat, directed=False) + + merged_obj_anns = [] + for i_comp in range(n_comp): + inds_merge = np.where(comp_labels == i_comp)[0].tolist() + polygons, bbox = merge_segmentations(img_anns, [obj_anns[i] for i in inds_merge]) + merged_obj_anns.append({ + "area": bbox[2] * bbox[3], + "bbox": bbox.tolist(), + "segmentation": polygons, + "iscrowd": 0, + "image_id": orig_img_ann["id"], + "id": obj_anns[inds_merge[0]]["id"], + "category_id": obj_anns[inds_merge[0]]["category_id"] + }) + + return orig_img, orig_img_ann, merged_obj_anns + +def merge_sliding_window_predictions( + img_dir: str, + ann_path: str, + out_dir: str, + task: str, + iou_threshold: float = 0.5, + ios_threshold: float = 0, + intersection_threshold: float = 100, + on_out_dir_exists: Callable[[str], bool] = lambda out_dir: True, + on_img_out_dir_exists: Callable[[str], bool] = lambda img_out_dir: True, +): + '''merge_sliding_window_predictions + + Merge predicted annotations that were based on sliding-window cropped images. + + Parameters + ---------- + img_dir : str + Path to directory containing the images that the prediction was made for. + ann_path : str + Path to predicted annotation. + out_dir : str + Path to directory that the merged annotations and images should be written to. + task : str + Either 'bbox-detection' or 'instance-segmentation'. + iou_threshold : float + Min. intersection over union of two objects to be merged. + Note that the denominator (union) is only calculated within the overlapping region + of two sliding windows. + ios_threshold : float + Min. intersection over smaller area. + The latter considers the object within the whole sliding window, not only within + the window overlap. + intersection_threshold : float + Min. absolute intersection. + on_out_dir_exists : Callable[[str], bool], optional + A function that decides what should happen if out_dir already exists. + If true is returned, out_dir will be overwritten. + By default, out_dir will be overwritten. + on_img_out_dir_exists : Callable[[str], bool], optional + A function that decides what should happen if img_out_dir already exists. + If true is returned, img_out_dir will be overwritten. + By default, img_out_dir will be overwritten. + ''' + ann = load_coco_ann(ann_path) + + bname_to_img_anns = defaultdict(list) + for img_ann in ann["images"]: + bname = get_bname_from_fname(img_ann["file_name"]) + bname_to_img_anns[bname].append(img_ann) + + img_id_to_obj_anns = defaultdict(list) + for obj_ann in ann["annotations"]: + img_id_to_obj_anns[obj_ann["image_id"]].append(obj_ann) + + if os.path.exists(out_dir): + should_remove = on_out_dir_exists(out_dir) + if should_remove: + shutil.rmtree(out_dir) + os.mkdir(out_dir) + else: + os.mkdir(out_dir) + + img_out_dir = os.path.join(out_dir, 'images') + if os.path.exists(img_out_dir): + should_remove = on_img_out_dir_exists(img_out_dir) + if should_remove: + shutil.rmtree(img_out_dir) + os.mkdir(img_out_dir) + else: + os.mkdir(img_out_dir) + + ann_out_file = os.path.join(out_dir, 'annotations.json') + + new_img_anns = [] + new_obj_anns = [] + + with TemporaryDirectory() as tmp_dir: + for bname in bname_to_img_anns: + img_anns = [] # without padding + obj_anns = [] + + for img_ann in bname_to_img_anns[bname]: + img_ann_new = copy.deepcopy(img_ann) + obj_anns_win = copy.deepcopy(img_id_to_obj_anns[img_ann["id"]]) + width_orig, height_orig = get_size_from_fname(img_ann['file_name']) + x0, x1, y0, y1 = get_coords_from_fname(img_ann['file_name']) + + # remove padding + if any((x0 < 0, y0 < 0, x1 > width_orig, y1 > height_orig)) : + X0 = -min(0, x0) + Y0 = -min(0, y0) + X1 = x1 - x0 - max(0, x1 - width_orig) + Y1 = y1 - y0 - max(0, y1 - height_orig) + + _, obj_anns_win = crop_annotations( + annotations = obj_anns_win, + img_width = img_ann["width"], + img_height = img_ann["height"], + cropping_range = (X0, X1, Y0, Y1), + start_id = None, + task = task, + keep_incomplete = True + ) + img_name_new = '{}_{}x{}_{}-{}_{}-{}.jpg'.format( + bname, + width_orig, + height_orig, + *np.clip((x0, x1), 0, width_orig).tolist(), + *np.clip((y0, y1), 0, height_orig).tolist() + ) + img_padded = cv2.imread(os.path.join(img_dir, img_ann['file_name'])) + cv2.imwrite( + os.path.join(tmp_dir, img_name_new), + crop_image(img_padded, (X0, X1, Y0, Y1)) + ) + img_ann_new["file_name"] = img_name_new + img_ann_new["width"] = X1 - X0 + img_ann_new["height"] = Y1 - Y0 + else: + os.symlink( + os.path.abspath(os.path.join(img_dir, img_ann["file_name"])), + os.path.join(tmp_dir, img_ann["file_name"]) + ) + + img_anns.append(img_ann_new) + obj_anns.extend(obj_anns_win) + + if task == "bbox-detection": + orig_img, orig_img_ann, merged_obj_anns = merge_window_predictions_bbox( + img_anns, + obj_anns, + tmp_dir, + iou_threshold = iou_threshold, + ios_threshold = ios_threshold, + intersection_threshold = intersection_threshold + ) + elif task == "instance-segmentation": + orig_img, orig_img_ann, merged_obj_anns = merge_window_predictions_seg( + img_anns, + obj_anns, + tmp_dir, + iou_threshold = iou_threshold, + ios_threshold = ios_threshold, + intersection_threshold = intersection_threshold + ) + + img_f = os.path.join(img_out_dir, f'{bname}.jpg') + cv2.imwrite(img_f, orig_img) + + new_img_anns.append(orig_img_ann) + new_obj_anns.extend(merged_obj_anns) + + new_ann = coco_utils.build_coco_dataset( + annotations=new_obj_anns, + images=new_img_anns, + categories=ann['categories'], + licenses=ann['licenses'], + info=ann['info'] + ) + + with open(ann_out_file, 'w') as ann_f: + json.dump(new_ann, ann_f, indent=2, cls=NumpyEncoder) diff --git a/ginjinn/utils/utils.py b/ginjinn/utils/utils.py new file mode 100644 index 0000000..3c63537 --- /dev/null +++ b/ginjinn/utils/utils.py @@ -0,0 +1,1126 @@ +""" +Module for generic helper functions. +""" + +from collections import defaultdict +import sys +import json +import glob +import os +import xml +import xml.etree.ElementTree as et +from typing import List, Sequence, Tuple +import imantics +import numpy as np +from pycocotools import mask as pmask + +def coco_seg_to_mask(seg, width, height): + """coco_seg_to_mask + + Convert segmentation annotation (either list of polygons or COCO's compressed RLE) + to binary mask. + + Parameters + ---------- + seg : dict or list + Segmentation annotation + width : int + Image/mask width + height : int + Image/mask height + + Returns + ------- + seg_mask : np.ndarray + Boolean segmentation mask + + Raises + ------ + TypeError + Raised for unsupported annotation formats. + """ + if isinstance(seg, dict): + # compressed rle to mask + seg_mask = pmask.decode(seg).astype("bool") + elif isinstance(seg, list): + # polygon to mask + polygons = imantics.Polygons(seg) + seg_mask = polygons.mask(width, height).array + else: + raise TypeError( + "Unknown segmentation format, polygons or compressed RLE expected" + ) + return seg_mask + +def bbox_from_mask(mask: np.ndarray, fmt: str): + """Calculate bounding box from segmentation mask. + + Parameters + ---------- + mask : np.ndarray + Segmentation mask + fmt : str + Output format, either "xywh" (COCO-like) or "xyxy" (PascalVoc-like) + + Returns + ------- + np.ndarray + Bounding box + + Raises + ------ + ValueError + Raised for unsupported output formats. + """ + x_any = mask.any(axis=0) + y_any = mask.any(axis=1) + x = np.where(x_any)[0].tolist() + y = np.where(y_any)[0].tolist() + + if len(x) > 0 and len(y) > 0: + x1, y1, x2, y2 = (x[0], y[0], x[-1] + 1, y[-1] + 1) + else: + x1, y1, x2, y2 = (0, 0, 0, 0) + + if fmt == "xywh": + bbox = ( x1, y1, x2 - x1, y2 - y1 ) + elif fmt == "xyxy": + bbox = ( x1, y1, x2, y2 ) + else: + raise ValueError( + f"Unknown bounding box format \"{fmt}\"." + ) + return np.array(bbox).astype("int") + +def bbox_from_polygons(polygons: List[List[float]], fmt: str): + """Calculate bounding box from polygons. + + Parameters + ---------- + polygons : list of list of float + List of polygons, i.e. [[x0, y0, x1, y1, x2, y2 ...], ...] + fmt : str + Output format, either "xywh" (COCO-like) or "xyxy" (PascalVoc-like) + + Returns + ------- + np.ndarray + Bounding box + + Raises + ------ + ValueError + Raised for unsupported output formats. + """ + if any(len(p) for p in polygons): + x = np.concatenate([p[0::2] for p in polygons]) + y = np.concatenate([p[1::2] for p in polygons]) + x0 = np.min(x) + x1 = np.max(x) + 1 + y0 = np.min(y) + y1 = np.max(y) + 1 + else: + x0, y0, x1, y1 = (0, 0, 0, 0) + + if fmt == "xywh": + bbox = ( x0, y0, x1 - x0, y1 - y0 ) + elif fmt == "xyxy": + bbox = ( x0, y0, x1, y1 ) + else: + raise ValueError( + f"Unknown bounding box format \"{fmt}\"." + ) + return np.array(bbox).round().astype("int") + +def confirmation(question: str) -> bool: + """Ask question expecting "yes" or "no". + + Parameters + ---------- + question : str + Question to be printed + + Returns + ------- + bool + True or False for "yes" or "no", respectively + """ + valid = {"yes": True, "y": True, "no": False, "n": False} + + while True: + choice = input(question + " [y/n]\n").strip().lower() + if choice in valid.keys(): + return valid[choice] + print("Please type 'yes' or 'no'\n") + +def confirmation_cancel(question: str) -> bool: + '''Ask question expecting 'yes' or 'no'. + + Parameters + ---------- + question : str + Question to be printed + + Returns + ------- + bool + True or False for 'yes' or 'no', respectively + ''' + valid = {'yes': True, 'y': True, 'no': False, 'n': False} + cancel = ['c', 'cancel', 'quit', 'q'] + + while True: + choice = input(question + ' [y(es)/n(o)/c(ancel)]\n').strip().lower() + if choice in valid.keys(): + return valid[choice] + elif choice in cancel: + sys.exit() + print('Please type "yes" or "no" (or "cancel")\n') + +def get_image_files( + img_dir: str, + img_file_extensions: List[str] = [ + '*.jpg', '*.JPG', '*.jpeg', '*.JPEG', + '*.png', '*.PNG', + ], +) -> List[str]: + '''get_image_files + + Get paths of image files in img_dir. + + Parameters + ---------- + img_dir : str + Directory containing images. + img_file_extensions : List[str], optional + Image file extensions, + by default [ '*.jpg', '*.JPG', '*.jpeg', '*.JPEG', '*.png', '*.PNG', ] + + Returns + ------- + List[str] + List of image file paths. + ''' + img_files = [] + for ext in img_file_extensions: + img_files.extend(glob.glob(os.path.join(img_dir, ext))) + return img_files + +def load_coco_ann(ann_path: str) -> dict: + '''load_coco_ann + + Load coco JSON annotation into dict. + + Parameters + ---------- + ann_path : str + Path to annotation JSON file. + + Returns + ------- + dict + [COCO annotation as dict. + ''' + with open(ann_path) as ann_f: + ann = json.load(ann_f) + return ann + +def get_obj_anns(img_ann, ann: dict) -> List[dict]: + '''get_obj_anns + + Get object annotations for image from COCO annotation dict. + + Parameters + ---------- + img_ann + Image id or image COCO dict. + ann : dict + COCO annotation ("dataset"). + + Returns + ------- + List[dict] + List of COCO object annotations for img_ann. + ''' + if isinstance(img_ann, int): + img_id = img_ann + else: + img_id = img_ann['id'] + + obj_anns = [obj_ann for obj_ann in ann['annotations'] if obj_ann['image_id'] == img_id] + return obj_anns + +def plot_coco_annotated_img(img, obj_anns: List[dict], ax=None): + '''plot_coco_annotated_img + + Plot COCO annotations on image. + + Parameters + ---------- + img + Image, numpy array. + obj_anns : List[dict] + List of COCO object annotation dicts. + ax + pyplot axis to plot on, by default None. + + Returns + ------- + pyplot axis + ''' + + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots() + + ax.imshow(img) + overlay_coco_annotations(obj_anns, ax) + + return ax + +def overlay_coco_annotations( + obj_anns: List[dict], + ax +): + '''overlay_coco_annotations + + Overlay coco annotations. + + Parameters + ---------- + obj_anns : List[dict] + List of COCO object annotation dicts. + ax + pyplot axis to plot on. + + Returns + ------- + pyplot axis + ''' + + import matplotlib.pyplot as plt + from matplotlib.patches import Rectangle, Polygon + + for ann in obj_anns: + print(ann) + seg = ann.get('segmentation', None) + if seg: + poly = Polygon( + np.array(seg).reshape(-1, 2), + fill='orange', + edgecolor='orange', + alpha=0.5, + ) + ax.add_patch(poly) + + bbox = ann.get('bbox', None) + if bbox: + rect = Rectangle( + bbox[0:2], bbox[2], bbox[3], + facecolor=None, + fill=False, + edgecolor='orange' + ) + ax.add_patch(rect) + + return ax + +def plot_pvoc_annotated_img( + img: np.ndarray, + ann: xml.etree.ElementTree.ElementTree, + ax=None, +): + '''plot_pvoc_annotated_img + + Plot PVOC annotated image. + + Parameters + ---------- + img : np.ndarray + Image as numpy array + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + ax + matplotlib axis, by default None + + Returns + ------- + matplotlib axis + ''' + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots() + + ax.imshow(img) + overlay_pvoc_ann(ann, ax) + + return ax + +def overlay_pvoc_ann( + ann: xml.etree.ElementTree.ElementTree, + ax, +): + '''overlay_pvoc_ann + + Plot PVOC annotation on ax. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + ax + matplotlib axis + + Returns + ------- + matplotlib axis + ''' + import matplotlib.pyplot as plt + from matplotlib.patches import Rectangle + + for obj in get_pvoc_objects(ann): + bbox = get_pvoc_obj_bbox(obj) + w, h = bbox_size(bbox) + rect = Rectangle( + [bbox[0], bbox[1]], w, h, + facecolor=None, + fill=False, + edgecolor='orange' + ) + ax.add_patch(rect) + + return ax + +def load_pvoc_annotation( + ann_path: str, +) -> xml.etree.ElementTree.ElementTree: + '''load_pvoc_annotation + + Load PVOC annotations from file. + + Parameters + ---------- + ann_path : str + PVOC annotation XML file path + + Returns + ------- + xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + ''' + return et.parse(ann_path) + +def write_pvoc_annotation( + ann: xml.etree.ElementTree.ElementTree, + ann_file: str, +): + '''write_pvoc_annotation + + Write PVOC annotation in ElementTree format to XML file. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + ann_file : str + Path to annotation XML file + ''' + import xml.dom.minidom as minidom + + root = ann.getroot() + xmlstr = minidom.parseString(et.tostring(root)).toprettyxml(indent=' ') + with open(ann_file, 'w') as ann_f: + ann_f.write(xmlstr) + +def clip_bbox( + bbox: Sequence[float], + clipping_range: Sequence[float], +) -> Sequence[float]: + '''clip_bbox + + Clip bounding box. + + Parameters + ---------- + bbox : Sequence[float] + Bounding-box in x0y0x1y1 format. + clipping_range : Sequence[float] + Clipping range in x0x1y0y1 format. + + Returns + ------- + Sequence[float] + Clipped bounding box in x0y0x1y1 format. + ''' + xmn, xmx, ymn, ymx = clipping_range + bbox_clipped = np.clip( + bbox, + [xmn, ymn, xmn, ymn], + [xmx, ymx, xmx, ymx], + ) + return bbox_clipped + +def crop_bbox( + bbox: Sequence[float], + cropping_range: Sequence[float], +) -> Sequence[float]: + '''crop_bbox + + Crop bounding box. Clips bbox and converts coordinates + to local coordinates in cropping range. + + Parameters + ---------- + bbox : Sequence[float] + Bounding-box in x0y0x1y1 format. + cropping_range : Sequence[float] + Cropping range in x0x1y0y1 format. + + Returns + ------- + Sequence[float] + Cropped bounding box in x0y0x1y1 format. + ''' + + bbox_clipped = clip_bbox(bbox, cropping_range) + bbox_cropped = [ + bbox_clipped[0] - cropping_range[0], + bbox_clipped[1] - cropping_range[2], + bbox_clipped[2] - cropping_range[0], + bbox_clipped[3] - cropping_range[2], + ] + + return bbox_cropped + +def bbox_size( + bbox: Sequence[float], +) -> Tuple[float, float]: + '''bbox_size + + Calculate bounding box size (width, height). + + Parameters + ---------- + bbox : Sequence[float] + Bounding-box in x0y0x1y1 format. + + Returns + ------- + Tuple[float, float] + Tuple of (width, height) + ''' + return ( + bbox[2] - bbox[0], + bbox[3] - bbox[1], + ) + +def bbox_area( + bbox: Sequence[float], +) -> float: + '''bbox_area + + Calculate bounding-box area. + + Parameters + ---------- + bbox : Sequence[float] + Bounding-box in x0y0x1y1 format. + + Returns + ------- + float + Area of the bounding-box. + ''' + w, h = bbox_size(bbox) + return w * h + +def get_pvoc_filename( + ann: xml.etree.ElementTree.ElementTree, +) -> str: + '''get_pvoc_filename + + Get image file name from PVOC annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + + Returns + ------- + str + Image file name. + ''' + return ann.find('filename').text + +def set_pvoc_filename( + ann: xml.etree.ElementTree.ElementTree, + filename: str, +): + '''set_pvoc_filename + + Set image file name for PVCO annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + filename : str + Image file name + ''' + ann.find('filename').text = filename + +def get_pvoc_size( + ann: xml.etree.ElementTree.ElementTree, +) -> Sequence[int]: + '''get_pvoc_size + + Get size of annotated image from PVOC annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + + Returns + ------- + Sequence[int] + Image size as Tuple (width, height, depth) + ''' + size_node = ann.find('size') + + # necessary to deal with PVOC exported by CVAT + try: + depth = int(size_node.find('depth').text) + except TypeError: + depth = 3 + + return [ + int(size_node.find('width').text), + int(size_node.find('height').text), + depth, + ] + +def set_pvoc_size( + ann: xml.etree.ElementTree.ElementTree, + size: Sequence[int], +): + '''set_pvoc_size + + Set size value for PVOC annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + size : Sequence[int] + Size as sequence of width, height, depth. + ''' + size_node = ann.find('size') + size_node.find('width').text = str(size[0]) + size_node.find('height').text = str(size[1]) + size_node.find('depth').text = str(size[2]) + +def get_pvoc_objects( + ann: xml.etree.ElementTree.ElementTree, +) -> List[xml.etree.ElementTree.ElementTree]: + '''get_pvoc_objects + + Get a list of PVCO annotation objects in ElementTree format. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + + Returns + ------- + List[xml.etree.ElementTree.ElementTree] + List of PVOC objects as ElementTree + ''' + return ann.findall('object') + +def add_pvoc_object( + ann: xml.etree.ElementTree.ElementTree, + obj: xml.etree.ElementTree.ElementTree, +): + '''add_pvoc_object + + Add PVOC object to PVOC annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + obj : xml.etree.ElementTree.ElementTree + PVOC object as ElementTree + ''' + r = ann.getroot() + r.append(obj) + +def drop_pvoc_objects( + ann: xml.etree.ElementTree.ElementTree, +): + '''drop_pvoc_objects + + Remove all objects from PVOC annotation. + + Parameters + ---------- + ann : xml.etree.ElementTree.ElementTree + PVOC annotation as ElementTree + ''' + r = ann.getroot() + for o in get_pvoc_objects(ann): + r.remove(o) + +def get_pvoc_obj_bbox( + obj: xml.etree.ElementTree.ElementTree, +) -> Sequence[float]: + '''get_pvoc_obj_bbox + + Get bounding-box from PVOC object. + + Parameters + ---------- + obj : xml.etree.ElementTree.ElementTree + PVOC object as ElementTree + + Returns + ------- + Sequence[float] + Bounding-box in x0y0x1y1 format. + ''' + bbox_node = obj.find('bndbox') + bbox = [ + float(bbox_node.find('xmin').text), + float(bbox_node.find('ymin').text), + float(bbox_node.find('xmax').text), + float(bbox_node.find('ymax').text), + ] + return bbox + +def get_pvoc_obj_name( + obj: xml.etree.ElementTree.ElementTree, +) -> str: + '''get_pvoc_obj_name + + Get name from PVOC object. + + Parameters + ---------- + obj : xml.etree.ElementTree.ElementTree + PVOC object as ElementTree + + Returns + ------- + str + Name of the PVOC object. + ''' + + return obj.find('name').text + +def set_pvoc_obj_bbox( + obj: xml.etree.ElementTree.ElementTree, + bbox: Sequence[float], +): + '''set_pvoc_obj_bbox + + Set bounding-box for PVOC object. + + Parameters + ---------- + obj : xml.etree.ElementTree.ElementTree + PVOC object as ElementTree + bbox : Sequence[int] + Bounding-box in x0y0x1y1 format. + ''' + bbox_node = obj.find('bndbox') + bbox_node.find('xmin').text = str(bbox[0]) + bbox_node.find('ymin').text = str(bbox[1]) + bbox_node.find('xmax').text = str(bbox[2]) + bbox_node.find('ymax').text = str(bbox[3]) + +def visualize_annotations( + ann_path: str, + img_dir: str, + out_dir: str, + ann_type: str, + vis_type: str, +): + '''visualize_annotations + + Visualize COCO and PVOC object annotations for PVOC and COCO annotated images. + + Parameters + ---------- + ann_path: str + Path to annotations JSON file for a COCO data set or path to a directory + containing XML annotations files for a PVOC data set. + img_dir: str + Path to a directory containing images corresponding to annotations in + ann_path. + out_dir : str + Path to an existing directory, which the visualizations should be written to. + ann_type: str + Type of annotation. Either "COCO" or "PVOC". + vis_type : str + Type of visualization. Either "segmentation" or "bbox". For PVOC annotations, + only "bbox" is allowed. + + Raises + ------ + Exception + Raised if an unknown visualization type is passed. + Exception + Raised if ann_type is "PVOC" and vis_type is "segmentation". + Exception + Raised if an unknown annotation type is passed. + ''' + + import cv2 + from ginjinn.data_reader.load_datasets import \ + MetadataCatalog, DatasetCatalog, load_vis_set, \ + get_class_names_coco, get_class_names_pvoc + from ginjinn.predictor.predictors import GJ_Visualizer, ColorMode + from detectron2.structures.boxes import BoxMode + from .sliding_window_merging import xywh_to_xyxy + + # input sanity checks + if not vis_type in ['bbox', 'segmentation']: + msg = f'Unknown visualization type "{vis_type}".' + raise Exception(msg) + + if vis_type == 'segmentation' and ann_type == 'PVOC': + msg = 'Visualization type "segmentation" is incompatible with annotation type "PVOC".' + raise Exception(msg) + + # get class names + if ann_type == 'COCO': + class_names = get_class_names_coco([ann_path]) + elif ann_type == 'PVOC': + class_names = get_class_names_pvoc([ann_path]) + else: + msg = f'Unknown annotation type "{ann_type}".' + raise Exception(msg) + + # load data set for visualization + load_vis_set(ann_path, img_dir, ann_type) + # filter annotations for images in img_path + vis_set = [ann for ann in DatasetCatalog.get('vis') if os.path.isfile(ann['file_name'])] + metadata = MetadataCatalog.get('vis') + + for img_ann in vis_set: + img = cv2.imread(img_ann['file_name']) + classes = np.array([ann['category_id'] for ann in img_ann['annotations']], dtype=int) + + # get bboxes + boxes = [ann['bbox'] for ann in img_ann['annotations']] + box_modes = [ann['bbox_mode'] for ann in img_ann['annotations']] + boxes = np.array([ + (xywh_to_xyxy(box) if b_mode == BoxMode.XYWH_ABS else box) + for box, b_mode in zip(boxes, box_modes) + ]) + + # get segmentation masks if available + if vis_type == 'segmentation': + segmentations = [ann['segmentation'] for ann in img_ann['annotations']] + masks = np.array([ + imantics.Polygons(seg).mask(img.shape[1], img.shape[0]).array + for seg in segmentations + ]) + else: + masks = None + + # visualize bboxes and segmentation masks + gj_vis = GJ_Visualizer( + img[:, :, ::-1], + metadata=metadata, + instance_mode=ColorMode.IMAGE_BW, + ) + vis_img = gj_vis.draw_instance_predictions_gj( + None, + classes, + boxes, + class_names, + masks = masks + ) + + vis_img.save( + os.path.abspath(os.path.join(out_dir, os.path.basename(img_ann['file_name']))) + ) + +def dataset_info( + ann_path: str, + img_dir: str, + ann_type: str, +): + '''dataset_info + + Parameters + ---------- + ann_path: str + Path to annotations JSON file for a COCO data set or path to a directory + containing XML annotations files for a PVOC data set. + img_dir: str + Path to a directory containing images corresponding to annotations in + ann_path. + ann_type: str + Type of annotation. Either "COCO" or "PVOC". + ''' + from ginjinn.data_reader.load_datasets import \ + MetadataCatalog, DatasetCatalog, load_vis_set + import pandas as pd + + # load data set + load_vis_set(ann_path, img_dir, ann_type) + metadata = MetadataCatalog.get('vis') + + count_df = pd.DataFrame( + columns=['#seg', '#bbox'], + index=[*metadata.thing_classes], + data=0 + ) + + img_count = 0 + for ann in DatasetCatalog.get('vis'): + img_count += 1 + objs = ann['annotations'] + for obj in objs: + if obj.get('segmentation'): + count_df.iloc[obj['category_id'], 0] += 1 + elif obj.get('bbox'): + count_df.iloc[obj['category_id'], 1] += 1 + else: + print(f'Invalid object annotation: {obj}') + + count_df.loc['total']= count_df.sum(numeric_only=True, axis=0) + count_df.loc[:,'total'] = count_df.sum(numeric_only=True, axis=1) + + empty_cls_idc = count_df['total'] == 0 + + print() + print('Dataset info for dataset') + print(f'\tann_path: {ann_path}') + print(f'\timg_dir: {img_dir}') + + print() + print('# images:', img_count) + print() + print('category distribution:') + print(count_df) + + if empty_cls_idc.any(): + print('') + print(' Found categories without annotation') + for cls_nm in count_df.index[empty_cls_idc]: + print(f'\t- "{cls_nm}"') + print() + print( + 'Please remove empty categories (ginjinn utils filter) ' +\ + 'if you are planning to use this dataset for model training.' + ) + +def count_categories( + ann_path: str, +) -> "pandas.DataFrame": + '''count_categories + + Parameters + ---------- + ann_path: str + Path to COCO JSON annotation file. + + Returns + ------- + "pandas.DataFrame" + Count dataframe. + ''' + import pandas as pd + + ann = load_coco_ann(ann_path) + + categories = sorted( + [(cat['name'], cat['id']) for cat in ann['categories']], + key = lambda x: x[1], + ) + cat_map = {cat[1]: cat[0] for cat in categories} + + columns = {cat[0]: [] for cat in categories} + index = [] + + for img_ann in ann['images']: + index.append(img_ann['file_name']) + + counts = {key: 0 for key in columns.keys()} + for obj_ann in get_obj_anns(img_ann, ann): + counts[cat_map[obj_ann['category_id']]] += 1 + + for key, count in counts.items(): + columns[key].append(count) + + count_df = pd.DataFrame( + data=columns, + index=index, + ) + count_df.index.set_names(['image']) + + return count_df + +def count_categories_pvoc( + ann_path: str, +) -> "pandas.DataFrame": + '''count_categories_pvoc + + Parameters + ---------- + ann_path: str + Path to PVOC annotation folder. + + Returns + ------- + "pandas.DataFrame" + Count dataframe. + ''' + + import pandas as pd + from glob import glob + ann_files = glob(os.path.join(ann_path, '*.xml')) + + index = [] + rows = [] + for ann_file in ann_files: + ann = load_pvoc_annotation(ann_file) + img_name = get_pvoc_filename(ann) + objs = get_pvoc_objects(ann) + counts = defaultdict(int) + for obj in objs: + obj_name = get_pvoc_obj_name(obj) + counts[obj_name] += 1 + rows.append(counts) + index.append(img_name) + + count_df = pd.DataFrame(rows, index, dtype='Int64').fillna(0) + count_df.index.set_names(['image']) + + return count_df + + +class InvalidAnnotationPath(Exception): + pass + +class InvalidDatasetDir(Exception): + pass + +class ImageDirNotFound(Exception): + pass + +def get_anntype(ann_path: str) -> str: + '''get_anntype + + Get annotation type (COCO or PVOC) of ann_path. + + Parameters + ---------- + ann_path : str + Path to JSON file for COCO or a folder for PVOC. + + Returns + ------- + str + Annotation type. Either "COCO" or "PVOC". + + Raises + ------ + InvalidAnnotationPath + Raised if ann_path is not a valid annotation path. + ''' + if os.path.isfile(ann_path): + return 'COCO' + elif os.path.isdir(ann_path): + return 'PVOC' + else: + msg = f'"{ann_path}" is not a valid annotation path.' + raise InvalidAnnotationPath(msg) + +def find_img_dir(ann_path: str) -> str: + '''find_img_dir + + Find images directory for ann_path. + + Parameters + ---------- + ann_path : str + Path to JSON annotation file (COCO) or folder (PVOC). + + Returns + ------- + str + Path to images directory. + + Raises + ------ + ImageDirNotFound + Raised if image directory could not be found. + ''' + ds_dir = os.path.dirname(os.path.abspath(ann_path)) + img_dir = os.path.join(ds_dir, 'images') + if not os.path.isdir(img_dir): + msg = f'Could not find "images" folder as a sibling to "{ann_path}".' + raise ImageDirNotFound(msg) + + return img_dir + +def get_dstype(data_dir: str) -> str: + '''get_dstype + + Get annotation type (COCO or PVOC) of dataset in data_dir. + + Parameters + ---------- + data_dir : str + Dataset directory. Must contain a folder named "images", and + a file named "annotations.json" for a COCO dataset or + a folder named "annotations" for a PVOC dataset. + + Returns + ------- + str + Dataset type. Either "COCO" or "PVOC". + + Raises + ------ + InvalidDatasetDir + Raised if data_dir is not a valid dataset directory. + ''' + + dir_content = os.listdir(data_dir) + if not 'images' in dir_content: + msg = f'Could not find "images" folder in "{data_dir}".' + raise InvalidDatasetDir(msg) + images_path = os.path.join(data_dir, 'images') + if not os.path.isdir(images_path): + msg = f'"{images_path}" is not a folder.' + raise InvalidDatasetDir(msg) + + # COCO + if 'annotations.json' in dir_content: + ann_path = os.path.join(data_dir, 'annotations.json') + if not os.path.isfile(ann_path): + msg = f'"{ann_path}" is not a file.' + raise InvalidDatasetDir(msg) + ds_type = 'COCO' + # PVOC + elif 'annotations' in dir_content: + ann_path = os.path.join(data_dir, 'annotations') + if not os.path.isdir(ann_path): + msg = f'"{ann_path}" is not a folder.' + raise InvalidDatasetDir(msg) + ds_type = 'PVOC' + else: + msg = f'Could not find annotations ("annotations" or "annotations.json") in {data_dir}.' + raise InvalidDatasetDir(msg) + + return ds_type diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..44394e1 --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +''' Setup script for GinJinn +''' + +import re +import glob +from setuptools import setup, find_packages + + +# get version from __init__.py file +with open('ginjinn/__init__.py', 'r') as f: + VERSION = re.search( + r"^__version__ = ['\"]([^'\"]*)['\"]", + f.read(), + re.M + ).group(1) + +DESCRIPTION = 'An object detection pipeline for the extraction of structures from herbarium specimens.' +AUTHOR = 'Tankred Ott, Ulrich Lautenschlager' +AUTHOR_EMAIL = 'tankred.ott@ur.de, ulrich.lautenschlager@ur.de' + +def install_requires(): + '''Get requirements from requirements.txt''' + # with open('requirements.txt') as f: + # return f.read().splitlines() + return [] + +setup( + name='ginjinn', + version=VERSION, + url='https://github.com/AGOberprieler/ginjinn', + author=AUTHOR, + author_email=AUTHOR_EMAIL, + description=DESCRIPTION, + packages=find_packages(), + install_requires=install_requires(), + entry_points={ + 'console_scripts': [ + 'ginjinn = ginjinn.commandline.main:main', + ] + }, + package_data={ + 'ginjinn': [ + 'data/ginjinn_config/template_config.yaml', + 'data/example_data.txt', + 'data/ginjinn_config/example_config_0.yaml', + 'data/ginjinn_config/example_config_1.yaml', + 'data/ginjinn_config/templates', + 'data/ginjinn_config/templates/*' + ], + } +)