├── src └── phenobench │ ├── evaluation │ ├── __init__.py │ ├── auxiliary │ │ ├── __init__.py │ │ ├── common.py │ │ ├── filter.py │ │ ├── convert.py │ │ └── panoptic_eval.py │ ├── phenobench_eval.py │ ├── evaluate_semantics.py │ ├── evaluate_leaf_instance_masks_panoptic.py │ ├── evaluate_leaf_bounding_boxes.py │ ├── evaluate_plant_bounding_boxes.py │ └── evaluate_plant_instance_masks_panoptic.py │ ├── __init__.py │ ├── tools │ └── validator.py │ ├── visualization.py │ └── phenobench_loader.py ├── .gitattributes ├── CITATION.cff ├── LICENSE ├── pyproject.toml ├── .gitignore ├── README.md └── phenobench_tutorial.ipynb /src/phenobench/evaluation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/phenobench/evaluation/auxiliary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/phenobench/__init__.py: -------------------------------------------------------------------------------- 1 | from .phenobench_loader import PhenoBench -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb filter=nbstripout 2 | *.zpln filter=nbstripout 3 | *.ipynb diff=ipynb 4 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | preferred-citation: 3 | title: "PhenoBench — A Large Dataset and Benchmarks for Semantic Image Interpretation in the Agricultural Domain" 4 | year: "2023" 5 | type: article 6 | journal: "arXiv" 7 | url: https://arxiv.org/pdf/2306.04557.pdf 8 | codeurl: https://github.com/PRBonn/phenobench-devkit 9 | authors: 10 | - family-names: Weyler 11 | given-names: Jan 12 | - family-names: Magistri 13 | given-names: Federico 14 | - family-names: Marks 15 | given-names: Elias 16 | - family-names: Chong 17 | given-names: Yue Linn 18 | - family-names: Sodano 19 | given-names: Matteo 20 | - family-names: Roggiolani 21 | given-names: Gianmarco 22 | - family-names: Chebrolu 23 | given-names: Nived 24 | - family-names: Stachniss 25 | given-names: Cyrill 26 | - family-names: Behley 27 | given-names: Jens -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jan Weyler, Federico Magistri, Elias Marks, Yue Linn Chong, 4 | Matteo Sodano, Gianmarco Roggiolani, Nived Chebrolu, Cyrill Stachniss, 5 | Jens Behley. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/phenobench/evaluation/auxiliary/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Dict, List, Tuple 4 | 5 | import numpy as np 6 | import torch 7 | import torchvision.transforms as T 8 | from numpy import genfromtxt 9 | from PIL import Image 10 | 11 | 12 | def is_png(filename: str) -> bool: 13 | return filename.endswith('.png') 14 | 15 | 16 | def is_txt(filename: str) -> bool: 17 | return filename.endswith('.txt') 18 | 19 | 20 | def get_png_files_in_dir(path_to_dir: Path) -> List[str]: 21 | filenames = sorted([filename for filename in os.listdir(path_to_dir) if is_png(filename)]) 22 | 23 | return filenames 24 | 25 | 26 | def get_txt_files_in_dir(path_to_dir: Path) -> List[str]: 27 | filenames = sorted([filename for filename in os.listdir(path_to_dir) if is_txt(filename)]) 28 | 29 | return filenames 30 | 31 | def load_file_as_int_tensor(path_to_file: Path) -> torch.Tensor: 32 | 33 | tensor = Image.open(path_to_file) 34 | tensor = np.asarray(tensor).astype(np.int32) 35 | tensor = torch.Tensor(tensor).type(torch.int32) 36 | 37 | return tensor 38 | 39 | def load_file_as_tensor(path_to_file: Path) -> torch.Tensor: 40 | to_tensor = T.ToTensor() 41 | 42 | tensor = Image.open(path_to_file) 43 | tensor = to_tensor(tensor) 44 | 45 | return tensor 46 | 47 | 48 | def load_yolo_txt_file(path_to_file: Path) -> torch.Tensor: 49 | annos = genfromtxt(path_to_file) 50 | 51 | return torch.Tensor(annos) 52 | 53 | def centered_text(text: str, line_width: int = 80, fill: str = '=') -> str: 54 | """ generate string such that text is center in given line width using the fill """ 55 | fill_width = (line_width - len(text)-2)//2 56 | repeat = fill_width // len(fill) 57 | 58 | return f"{fill*repeat} {text} {fill*repeat}" 59 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "phenobench" 3 | version = "0.1.0" 4 | description = "The official development kit of the PhenoBench dataset (www.phenobench.org)." 5 | readme = "README.md" 6 | license = {file = "LICENSE"} 7 | requires-python = ">=3.7" 8 | 9 | authors = [ 10 | {name = "Jens Behley", email = "jens.behley@igg.uni-bonn.de" }, 11 | {name = "Jan Weyler", email = "jan.weyler@igg.uni-bonn.de" }, 12 | {name = "Federico Magistri", email = "federico.magistri@uni-bonn.de" }, 13 | {name = "Matteo Sodano", email = "matteo.sodano@igg.uni-bonn.de" }, 14 | {name = "Gianmarco Roggiolani", email = "groggiol@uni-bonn.de" }, 15 | {name = "Elias Marks", email = "elias.marks@uni-bonn.de" }, 16 | {name = "Yue (Linn) Chong", email = "ychong@uni-bonn.de" }, 17 | ] 18 | 19 | maintainers = [ 20 | {name = "Jens Behley", email = "jens.behley@igg.uni-bonn.de" } 21 | ] 22 | 23 | 24 | classifiers = [ 25 | "Development Status :: 4 - Beta", 26 | "Operating System :: OS Independent", 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3 :: Only", 35 | ] 36 | 37 | dependencies = [ 38 | "numpy", 39 | "matplotlib", 40 | "Pillow>6.2.1" 41 | ] 42 | 43 | [project.optional-dependencies] 44 | eval = [ 45 | "torch", 46 | "torchvision", 47 | "torchmetrics==0.10.3", 48 | "numpy", 49 | "pyyaml", 50 | "tqdm" 51 | ] 52 | 53 | [project.scripts] 54 | phenobench-validator = "phenobench.tools.validator:run" 55 | phenobench-eval = "phenobench.evaluation.phenobench_eval:run" 56 | 57 | [project.urls] 58 | "Homepage" = "https://www.phenobench.org" 59 | "Bug Reports" = "https://github.com/PRBonn/phenobench/issues" 60 | "Source" = "https://github.com/PRBonn/phenobench" 61 | 62 | [build-system] 63 | requires = ["setuptools>=43.0.0", "wheel"] 64 | build-backend = "setuptools.build_meta" 65 | 66 | 67 | # tool configuration 68 | [tool.yapf] 69 | based_on_style = "pep8" 70 | column_limit = "120" 71 | indent_width = "2" 72 | 73 | [tool.isort] 74 | profile = "black" 75 | 76 | [tool.pylint.format] 77 | max-line-length = "120" 78 | indent-string = ' ' 79 | -------------------------------------------------------------------------------- /src/phenobench/evaluation/auxiliary/filter.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import torch 4 | 5 | from .convert import bounding_box_from_cxcywh_to_xyxy, box_to_mask 6 | 7 | 8 | def filter_partial_masks(pred_instance_masks: torch.Tensor, pred_semantics: torch.Tensor, gt_instance_masks: torch.Tensor, gt_semantics: torch.Tensor, gt_visibility: torch.Tensor): 9 | assert torch.max(gt_visibility) <= 1.0 10 | assert torch.min(gt_visibility) >= 0.0 11 | 12 | gt_instance_partial_masks = [] 13 | # set semantics of partial masks to 0 and store partial_masks 14 | for gt_instance_id in torch.unique(gt_instance_masks): 15 | if gt_instance_id == 0: 16 | continue 17 | gt_mask = gt_instance_masks == gt_instance_id 18 | gt_vis = torch.unique(gt_visibility[gt_mask]) # contain a single value 19 | if gt_vis > 0.5: 20 | continue 21 | gt_semantics[gt_mask] = 0 22 | gt_instance_partial_masks.append(gt_mask) 23 | 24 | 25 | for pred_instance_id in torch.unique(pred_instance_masks): 26 | if pred_instance_id == 0: 27 | continue 28 | pred_mask = pred_instance_masks == pred_instance_id 29 | 30 | for gt_mask in gt_instance_partial_masks: 31 | # compute how much of the prediction is within the ground truth 32 | a = torch.sum(gt_mask & pred_mask) 33 | b = torch.sum(pred_mask) 34 | score = a / (b + 1e-12) 35 | assert score <= 1.0 36 | 37 | if score > 0.5: 38 | pred_semantics[pred_mask] = 0 39 | 40 | def filter_partials_boxes(preds: Dict, targets: Dict): 41 | n_preds = preds['boxes'].shape[0] 42 | n_targets = targets['boxes'].shape[0] 43 | 44 | remove_pred_indicies = [] 45 | remove_gt_indicies = [] 46 | 47 | for i in range(n_preds): 48 | bbox_pred = preds['boxes'][i, :] 49 | cx, cy, width, height = bbox_pred[0].item(), bbox_pred[1].item(), bbox_pred[2].item(), bbox_pred[3].item() 50 | xmin, xmax, ymin, ymax = bounding_box_from_cxcywh_to_xyxy(cx, cy, width, height) 51 | bbox_pred_canvas = box_to_mask(xmin, xmax, ymin, ymax) 52 | 53 | for j in range(n_targets): 54 | vis = targets['visibility'][j] 55 | if vis > 0.5: 56 | continue 57 | remove_gt_indicies.append(j) 58 | 59 | bbox_target = targets['boxes'][j, :] 60 | cx, cy, width, height = bbox_target[0].item(), bbox_target[1].item(), bbox_target[2].item(), bbox_target[3].item() 61 | xmin, xmax, ymin, ymax = bounding_box_from_cxcywh_to_xyxy(cx, cy, width, height) 62 | bbox_gt_canvas = box_to_mask(xmin, xmax, ymin, ymax) 63 | 64 | # compute how much of the prediction is within the ground truth 65 | a = torch.sum(bbox_pred_canvas & bbox_gt_canvas) 66 | b = torch.sum(bbox_pred_canvas) 67 | score = a / (b + 1e-12) 68 | assert score <= 1.0 69 | 70 | if score > 0.5: 71 | remove_pred_indicies.append(i) 72 | 73 | # filter predictions 74 | mask_pred = torch.ones_like(preds['labels'], dtype=torch.bool) 75 | for rm_idx in remove_pred_indicies: 76 | mask_pred[rm_idx] = False 77 | preds['labels'] = preds['labels'][mask_pred] 78 | preds['scores'] = preds['scores'][mask_pred] 79 | preds['boxes'] = preds['boxes'][mask_pred] 80 | 81 | # filter targets 82 | mask_target = torch.ones_like(targets['labels'], dtype=torch.bool) 83 | for rm_idx in remove_gt_indicies: 84 | mask_target[rm_idx] = False 85 | targets['labels'] = targets['labels'][mask_target] 86 | targets['boxes'] = targets['boxes'][mask_target] 87 | targets['visibility'] = targets['visibility'][mask_target] 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/c++,code,python,virtualenv 3 | # Edit at https://www.gitignore.io/?templates=c++,code,python,virtualenv 4 | 5 | ### C++ ### 6 | # Prerequisites 7 | *.d 8 | 9 | # Compiled Object files 10 | *.slo 11 | *.lo 12 | *.o 13 | *.obj 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Compiled Dynamic libraries 20 | *.so 21 | *.dylib 22 | *.dll 23 | 24 | # Fortran module files 25 | *.mod 26 | *.smod 27 | 28 | # Compiled Static libraries 29 | *.lai 30 | *.la 31 | *.a 32 | *.lib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | 39 | ### Code ### 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | ### Python ### 47 | # Byte-compiled / optimized / DLL files 48 | __pycache__/ 49 | *.py[cod] 50 | *$py.class 51 | 52 | # C extensions 53 | 54 | # Distribution / packaging 55 | .Python 56 | build/ 57 | develop-eggs/ 58 | dist/ 59 | downloads/ 60 | eggs/ 61 | .eggs/ 62 | lib/ 63 | lib64/ 64 | parts/ 65 | sdist/ 66 | var/ 67 | wheels/ 68 | pip-wheel-metadata/ 69 | share/python-wheels/ 70 | *.egg-info/ 71 | .installed.cfg 72 | *.egg 73 | MANIFEST 74 | 75 | # PyInstaller 76 | # Usually these files are written by a python script from a template 77 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 78 | *.manifest 79 | *.spec 80 | 81 | # Installer logs 82 | pip-log.txt 83 | pip-delete-this-directory.txt 84 | 85 | # Unit test / coverage reports 86 | htmlcov/ 87 | .tox/ 88 | .nox/ 89 | .coverage 90 | .coverage.* 91 | .cache 92 | nosetests.xml 93 | coverage.xml 94 | *.cover 95 | .hypothesis/ 96 | .pytest_cache/ 97 | 98 | # Translations 99 | *.mo 100 | *.pot 101 | 102 | # Scrapy stuff: 103 | .scrapy 104 | 105 | # Sphinx documentation 106 | docs/_build/ 107 | 108 | # PyBuilder 109 | target/ 110 | 111 | # pyenv 112 | .python-version 113 | 114 | # pipenv 115 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 116 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 117 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 118 | # install all needed dependencies. 119 | #Pipfile.lock 120 | 121 | # celery beat schedule file 122 | celerybeat-schedule 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # Mr Developer 135 | .mr.developer.cfg 136 | .project 137 | .pydevproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | ### VirtualEnv ### 151 | # Virtualenv 152 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 153 | pyvenv.cfg 154 | .env 155 | .venv 156 | env/ 157 | venv/ 158 | ENV/ 159 | env.bak/ 160 | venv.bak/ 161 | pip-selfcheck.json 162 | 163 | # End of https://www.gitignore.io/api/c++,code,python,virtualenv 164 | 165 | ### JupyterNotebooks ### 166 | # gitignore template for Jupyter Notebooks 167 | # website: http://jupyter.org/ 168 | 169 | .ipynb_checkpoints 170 | */.ipynb_checkpoints/* 171 | 172 | # IPython 173 | profile_default/ 174 | ipython_config.py 175 | 176 | # Remove previous ipynb_checkpoints 177 | # git rm -r .ipynb_checkpoints/ 178 | 179 | .vscode 180 | image_labeler/widgets/ui_labeler.py 181 | data 182 | *.DS_Store 183 | html 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhenoBench Development Kit 2 | 3 | ![](https://www.phenobench.org/imgs/devkit_teaser.jpg) 4 | 5 | [PhenoBench](https://www.phenobench.org) is a large dataset and benchmarks for the semantic interpretation of images of real agricultural fields. Together with the dataset, we provide a development kit that provides: 6 | 7 | - a framework-agnostic data loader. 8 | - visualization functions for drawing our data format. 9 | - evaluation scripts, `phenobench-eval`, for all tasks (also used on the CodaLab servers). 10 | - validator, called `phenobench-validator`, checking CodaLab submission files for consistency 11 | 12 | For more information on the dataset, please visit [www.phenobench.org](https://www.phenobench.org). 13 | 14 | ## Getting started 15 | 16 | 1. [Download](https://www.phenobench.org/dataset.html) the dataset. 17 | 2. Install the development kit: `pip install phenobench`. 18 | 3. Explore the data with the [tutorial notebook](phenobench_tutorial.ipynb). 19 | 4. See the [code of our baselines](https://github.com/PRBonn/phenobench-baselines) as a starting point or train your own models. 20 | 5. See the [FAQ](#frequently-asked-questions) for common questions and troubleshooting. 21 | 22 | If you discover a problem or have general questions regarding the dataset, don't hesitate to open an issues. We will try to resolve your issue as quickly as possible. 23 | 24 | ## Evaluation scripts (`phenobench-eval`) 25 | 26 | **Important:** Install all dependencies with `pip install "phenobench[eval]"`. 27 | 28 | For evaluating and computing the metrics for a specific task, you can run the `phenobench-eval` tool as follows: 29 | 30 | ```bash 31 | $ phenobench-eval --task --phenobench_dir --prediction_dir --split 32 | ``` 33 | - `task` is one of the following options: `semantics`, `panoptic`, `leaf_instances`, `plant_detection`, `leaf_detection`, or `hierarchical`. 34 | - `phenobench_dir` is the root directory of the PhenoBench dataset, where `train`, `val` directories are located. 35 | - `prediction_dir` is the directory containing the predictions as sub-folders, which depend on the specific tasks. 36 | - `split` is either `train` or `val`. 37 | 38 | Note that **all ablation studies of your approach should run on the validation set**. Thus, we also provide a comparably large validation set to enable a solid comparison of different settings of your approach. 39 | 40 | ## CodaLab Submission Validator (`phenobench-validator`) 41 | 42 | Before you submit a zip file to our CodaLab competitions, see also our available [benchmarks](https://www.phenobench.org/benchmarks.html), you can use the `phenobench-validator` to check your submission for consistency. The tool is also part of the pip package, therefore after installing the package via pip, you can call the `phenobench-validator` as follows: 43 | 44 | ```bash 45 | $ phenobench-validator --task --phenobench_dir --zipfile 46 | ``` 47 | - `task` is one of the following options: `semantics`, `panoptic`, `leaf_instances`, `plant_detection`, `leaf_detection`, or `hierarchical`. 48 | - `phenobench_dir` is the root directory of the PhenoBench dataset, where `train`, `val` directories are located. 49 | - `zipfile` is the zip file that you want to submit to the corresponding benchmark on CodaLab. 50 | 51 | ## Frequently Asked Questions 52 | 53 | **Question:** What are the usage restrictions of the PhenoBench dataset? 54 | **Answer:** We distribute the dataset using the CC-BY-SA International 4.0 license, which allows research but also commercial usage as long as the dataset is properly attributed (via a citation of the corresponding paper) and distributed with the same license if altered or modified. See also our [dataset overview page](https://www.phenobench.org/dataset.html) for the full license text, etc. 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/phenobench/tools/validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | from zipfile import ZipFile 6 | 7 | import numpy as np 8 | 9 | 10 | class ValidationException(Exception): 11 | pass 12 | 13 | 14 | def run(): 15 | parser = argparse.ArgumentParser( 16 | description= 17 | "Validate a submission zip file needed to evaluate on CodaLab competitions of PhenoBench.\n\nThe validator checks:\n 1. correct folder structure,\n 2. existence of label files for each image.\nInvalid labels are ignored by the evaluation script, therefore we don't check\nfor invalid labels.", 18 | formatter_class=argparse.RawTextHelpFormatter) 19 | 20 | parser.add_argument( 21 | "--task", 22 | type=str, 23 | required=True, 24 | choices=["semantics", "panoptic", "leaf_instances", "plant_detection", "leaf_detection", "hierarchical"], 25 | help='task for which the zip file should be validated.') 26 | 27 | parser.add_argument( 28 | "--zipfile", 29 | type=str, 30 | required=True, 31 | help='zip file that should be validated.', 32 | ) 33 | 34 | parser.add_argument('--phenobench_dir', 35 | type=str, 36 | required=True, 37 | help='Root directory containing the folder train, val, test of the PhenoBench.') 38 | 39 | FLAGS, _ = parser.parse_known_args() 40 | 41 | checkmark = "\u2713" 42 | 43 | required_folders = { 44 | "semantics": ["semantics"], 45 | "panoptic": ["semantics", "plant_instances"], 46 | "leaf_instances": ["leaf_instances"], 47 | "plant_detection": ["plant_bboxes"], 48 | "leaf_detection": ["leaf_bboxes"], 49 | "hierarchical": ["semantics", "plant_instances", "leaf_instances"] 50 | } 51 | 52 | try: 53 | 54 | print('Validating zip archive "{}".\n'.format(FLAGS.zipfile)) 55 | 56 | print(" ========== {:^15} ========== ".format(FLAGS.task)) 57 | 58 | print(" 1. Checking filename.............. ", end="", flush=True) 59 | if not FLAGS.zipfile.endswith('.zip'): 60 | raise ValidationException('Competition submission must end with ".zip"') 61 | print(checkmark) 62 | 63 | with ZipFile(FLAGS.zipfile) as zipfile: 64 | 65 | print(" 2. Checking directory structure... ", end="", flush=True) 66 | 67 | directories = [folder.filename for folder in zipfile.infolist() if folder.filename.endswith("/")] 68 | for expected_folder in required_folders[FLAGS.task]: 69 | if expected_folder + "/" not in directories: 70 | raise ValidationException( 71 | f'Directory "{expected_folder}" missing inside zip file. Your zip file should contain following folders: {", ".join(required_folders[FLAGS.task])}' 72 | ) 73 | 74 | print(checkmark) 75 | 76 | print(' 3. Checking files................. ', end='', flush=True) 77 | 78 | prediction_files = {info.filename: info for info in zipfile.infolist() if not info.filename.endswith("/")} 79 | image_files = sorted(os.listdir(os.path.join(FLAGS.phenobench_dir, "test", "images"))) 80 | 81 | for expected_folder in required_folders[FLAGS.task]: 82 | if "bboxes" in expected_folder: 83 | for image_file in image_files: 84 | expected_file = f"{expected_folder}/{os.path.splitext(image_file)[0]}.txt" 85 | if expected_file not in prediction_files: 86 | raise ValidationException(f'Missing prediction for {image_file} in folder {expected_folder}!') 87 | else: 88 | for image_file in image_files: 89 | expected_file = f"{expected_folder}/{image_file}" 90 | if expected_file not in prediction_files: 91 | raise ValidationException(f'Missing prediction for {image_file} in folder {expected_folder}!') 92 | 93 | print(checkmark) 94 | 95 | except ValidationException as ex: 96 | print("\n\n " + "\u001b[1;31m>>> Error: " + str(ex) + "\u001b[0m") 97 | exit(1) 98 | 99 | print("\n\u001b[1;32mEverything ready for submission!\u001b[0m \U0001F389") 100 | 101 | 102 | if __name__ == "__main__": 103 | run() 104 | -------------------------------------------------------------------------------- /src/phenobench/visualization.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Tuple 2 | 3 | import matplotlib.patches as patches 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from matplotlib.axes import Axes 7 | from PIL import Image 8 | 9 | 10 | def draw_semantics(axes: Axes, 11 | image: Image, 12 | semantics: np.array, 13 | alpha: float = 0.5, 14 | colors: Dict[int, Tuple[int, int, int]] = { 15 | 1: (0, 255, 0), 16 | 2: (255, 0, 0), 17 | 3: (0, 255, 255), 18 | 4: (255, 0, 255) 19 | }, 20 | mask_classes: List[int] = [0]): 21 | """ draw pixel-wise semantics according to given image and semantics. 22 | 23 | Args: 24 | axes (matplotlib.axes.Axes): axes object to draw into. 25 | image (PIL.Image): image that should be drawn. 26 | semantics (np.array): semantics (1=crop, 2=weed, 3=partial crop, 4=partial weed) 27 | alpha(float): transparency of semantic mask drawn on the image. 28 | colors(Dict[int, tuple]): mapping of class ids to colors. Default: 1(crop) = (0,255,0), 2(weed) = (0,255,0), 3(partial_crop)= (0,255,255), 4(partial_weed) = (255,0,255) 29 | mask_classes (List[int]): class indices that are not shown in the final composition. 30 | """ 31 | 32 | im_semantics = np.zeros((*semantics.shape, 3), dtype=float) 33 | 34 | for label, color in colors.items(): 35 | im_semantics[semantics == label] = np.array(color) / 255.0 36 | 37 | im_alpha = np.ones((*semantics.shape, 3), dtype=float) * alpha 38 | for c in mask_classes: 39 | im_alpha[semantics == c] = 0.0 40 | 41 | composed_image = (1.0 - im_alpha) * (np.array(image) / 255.0) + im_alpha * im_semantics 42 | axes.imshow(composed_image) 43 | 44 | # not sure why, but `ax.im_show(im_semantics, alpha=im_alpha)` doesn't work as expected: 45 | # im_alpha = np.ones(semantics.shape, dtype=float) * alpha 46 | # for c in mask_classes: im_alpha[semantics == c] = 0.0 47 | # axes.imshow(image) 48 | # axes.imshow(im_semantics, alpha = im_alpha) 49 | 50 | 51 | def draw_instances(ax: Axes, image: Image, instance_mask: np.array, alpha: float = 0.5): 52 | """ Draw instance masks from provided data, where individual masks are indicated by random colors. 53 | 54 | Args: 55 | ax (Axes): axes object to draw into. 56 | image (Image): image that should be drawn. 57 | instance_masks (np.array): _description_ 58 | alpha (float, optional): _description_. Defaults to 0.5. 59 | """ 60 | random_colors = plt.get_cmap('tab10').resampled(10) 61 | unique_ids = np.unique(instance_mask) 62 | # unique_ids = unique_ids[unique_ids > 0] 63 | 64 | im_mask = np.zeros((*instance_mask.shape, 3)) 65 | for idx, uid in enumerate(unique_ids): 66 | im_mask[instance_mask == uid] = random_colors(idx % 10)[:3] 67 | 68 | im_alpha = np.ones((*instance_mask.shape, 3)) * alpha 69 | im_alpha[instance_mask == 0, :] = 0.0 70 | 71 | float_image = (np.array(image) / 255.0) 72 | composed_image = (1.0 - im_alpha) * float_image + im_alpha * im_mask 73 | 74 | ax.imshow(composed_image) 75 | 76 | 77 | def draw_bboxes(ax: Axes, 78 | image: Image, 79 | bboxes: List[Dict], 80 | colors: Dict[int, Tuple[int, int, int]] = { 81 | 1: (0, 255, 0), 82 | 2: (255, 0, 0), 83 | 3: (0, 255, 255), 84 | 4: (255, 0, 255) 85 | }, 86 | linewidth=2): 87 | """ Draw the given bounding boxes on the image. 88 | 89 | Args: 90 | ax (Axes): axes object to draw into. 91 | image (Image): image that should be drawn. 92 | bboxes (List[Dict): list of bounding boxes in the format {"label":