Added annotation widget notebook
This commit is contained in:
PatrickBue 2019-04-10 11:51:18 -04:00 коммит произвёл GitHub
Родитель 1b7daaccb0
Коммит 84ce55c86e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 684 добавлений и 3 удалений

Просмотреть файл

Просмотреть файл

@ -8,6 +8,7 @@
* [Which problems can be solved using image classification, and which ones cannot](#which-problems-can-be-solved-using-image-classification)
* Data
* [How many images are required to train a model?](#how-many-images-are-required-to-train-a-model)
* [How to collect a large set of images?](#how-to-collect-a-large-set-of-images)
* [How to annotate images?](#how-to-annotate-images)
* [How to split into training and test images?](#How-to-split-into-training-and-test-images)
* [How to design a good test set?](#how-to-design-a-good-test-set)
@ -15,6 +16,7 @@
* Training
* [How to improve accuracy or inference speed?](#how-to-improve-accuracy-or-inference-speed)
### How does the technology work?
State-of-the-art image classification methods such as used in this repository are based on Convolutional Neural Networks (CNN). CNNs are a special group of Deep Learning approaches shown to work well on image data. The key is to use CNNs which were already trained on millions of images (the ImageNet dataset) and to fine-tune these pre-trained CNNs using a potentially much smaller custom dataset. This is the approach also taken in this repository. The web is full of introductions to these conceptions, such as [link](https://towardsdatascience.com/simple-introduction-to-convolutional-neural-networks-cdf8d3077bac).
@ -29,6 +31,22 @@ This depends heavily on the complexity of the problem. For example, if the objec
In practice, we have seen good results using 100 images for each class or sometime less. The only way to find out how many images are required, is by training the model using increasing number of images, while observing how the accuracy improves (while keeping the test set fixed). Once accuracy improvements become small, this would indicate that more training images are not required.
### How to collect a large set of images?
Collecting a sufficiently large number of annotated images for training and testing can be difficult. One way to over-come this problem is to scrape images from the Internet. For example, see below (left image) the Bing Image Search results for the query "tshirt striped". As expected, most images indeed are striped t-shirts, and the few incorrect or ambiguous images (such as column 1, row 1; or column 3, row 2) can be identified and removed easily. Rather than manually downloading images from Bing Image Search, the [Cognitive Services Bing Image Search API](https://www.microsoft.com/cognitive-services/en-us/bing-image-search-api) (right image) can be used instead.
|Bing Image Search | Cognitive Services Image Search|
|:-------------------------:|:-------------------------:|
|<img src="media/bing_search_striped.jpg" alt="alt text" width="400"/> | <img src="media/bing_image_search_api.jpg" alt="alt text" width="400"/>|
To generate a large and diverse dataset, multiple queries should be used. For example 7\*3 = 21 queries can by synthesized using all combinations of 7 clothing items {blouse, hoodie, pullover, sweater, shirt, tshirt, vest} and 3 attributes {striped, dotted, leopard}. Downloading the top 50 images per query would then lead to a maximum of 21*50=1050 images.
Some of the downloaded images will be exact or near duplicates (e.g. differ just by image resolution or jpg artifacts) and should be removed so that the training and test split do not contain the same images. This can be achieved using a hashing-based approach which works in two steps: (i) first, the hash string is computed for all images; (ii) only images are kept with a hash string which has not yet been seen. All other images are discarded. We found the *dhash* approach in the Python library *imagehash* and described in this [blog](http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html) to perform well, with the parameter `hash_size` set to 16. It is OK to incorrectly remove some non-duplicates images, as long as the majority of the real duplicates get removed.
### How to annotate images?
Consistency is key. For example, occluded objects should either be always annotated, or never. Furthermore, ambiguous images should be removed, eg if it is unclear to a human eye if an image shows a lemon or a tennis ball. Ensuring consistency is difficult especially if multiple people are involved, and hence our recommendation is that only a single person, the one who trains the AI model, annotates all images. This has the added benefit of gaining a better understanding of the images and of the complexity of the classification task.

Просмотреть файл

Двоичные данные
image_classification/media/bing_image_search_api.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 314 KiB

Двоичные данные
image_classification/media/bing_search_striped.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 173 KiB

Просмотреть файл

@ -0,0 +1,184 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<i>Copyright (c) Microsoft Corporation. All rights reserved.</i>\n",
"\n",
"<i>Licensed under the MIT License.</i>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Image annotation UI"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Open-source annotation tools for object detection and for image segmentation exist, however for image classification we were not able to find a good program. Hence this notebook provides a simple UI to label images. Each image can be annotated with one or multiple classes, or marked as \"Exclude\" to indicate that the image should not be used for model training or evaluation. \n",
"\n",
"Note that, for single class annotation tasks, one does not need any UI but can instead simply drag-and-drop images into separate folder for the respective classes. \n",
"\n",
"See the [FAQ.md](..\\FAQ.md) for a brief discussion on how to scrape images from the internet."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"# Ensure edits to libraries are loaded and plotting is shown in the notebook.\n",
"%reload_ext autoreload\n",
"%autoreload 2\n",
"%matplotlib inline"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import os, sys\n",
"sys.path.append(\"../\")\n",
"from utils_ic.anno_utils import AnnotationWidget\n",
"from utils_ic.datasets import unzip_url, Urls"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Set parameters: location of the images to annotate, and path where to save the annotations. Here `unzip_url` is used to download example data if not already present, and set the path."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"tags": [
"parameters"
]
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Using images in directory: C:\\Users\\pabuehle\\Desktop\\ComputerVisionBestPractices\\image_classification\\data\\fridgeObjects\\can.\n"
]
}
],
"source": [
"IM_DIR = os.path.join((unzip_url(Urls.fridge_objects_path, exist_ok=True)), 'can')\n",
"ANNO_PATH = \"cvbp_ic_annotation.txt\"\n",
"print(f\"Using images in directory: {IM_DIR}.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Start the UI. Check the \"Allow multi-class labeling\" box to allow for images to be annotated with multiple classes. When in doubt what the annotation for an image should be, or for any other reason (e.g. blur or over-exposure), mark an image as \"EXCLUDE\". All annotations are saved to (and loaded from) a pandas dataframe with path specified in `anno_path`. \n",
"\n",
"<center>\n",
"<img src=\"media/anno_ui.jpg\" style=\"width: 600px;\"/>\n",
"<i>Annotation UI example</i>\n",
"</center>"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading existing annotation from cvbp_ic_annotation.txt.\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "492cae0bb3e340babb935f688660d11f",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Tab(children=(VBox(children=(HBox(children=(Button(description='Previous', layout=Layout(width='80px'), style=…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"w_anno_ui = AnnotationWidget(\n",
" labels = [\"can\", \"carton\", \"milk_bottle\", \"water_bottle\"],\n",
" im_dir = IM_DIR,\n",
" anno_path = ANNO_PATH,\n",
" im_filenames = None #Set to None to annotate all images in IM_DIR\n",
")\n",
"\n",
"display(w_anno_ui.show())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Below is an example how to create a fast.ai ImageList object using the ground truth annotations generated by the AnnotationWidget. Note that fast.ai does not support the exclude flag, hence we remove these images before calling fast.ai's `from_df()` and `label_from_df()` functions.\n",
"\n",
"```python\n",
"import pandas as pd\n",
"from fastai.vision import ImageList,ImageDataBunch\n",
"\n",
"# Load annotation, discard excluded images, and convert to format fastai expects\n",
"data = []\n",
"with open(ANNO_PATH,'r') as f:\n",
" for line in f.readlines()[1:]:\n",
" vec = line.strip().split(\"\\t\")\n",
" exclude = vec[1]==\"True\"\n",
" if not exclude and len(vec)>2:\n",
" data.append((vec[0], vec[2]))\n",
"\n",
"df = pd.DataFrame(data, columns = [\"name\", \"label\"])\n",
"display(df)\n",
"\n",
"data = (ImageList.from_df(path=IM_DIR, df = df)\n",
" .split_by_rand_pct(valid_pct=0.5)\n",
" .label_from_df(cols='label', label_delim=','))\n",
"```"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (cvbp)",
"language": "python",
"name": "cvbp"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.8"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

Двоичные данные
image_classification/notebooks/media/anno_ui.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 67 KiB

Просмотреть файл

@ -0,0 +1,85 @@
#!/usr/bin/env python
# coding: utf-8
# <i>Copyright (c) Microsoft Corporation. All rights reserved.</i>
#
# <i>Licensed under the MIT License.</i>
# # Image annotation UI
# Open-source annotation tools for object detection and for image segmentation exist, however for image classification we were not able to find a good program. Hence this notebook provides a simple UI to label images. Each image can be annotated with one or multiple classes, or marked as "Exclude" to indicate that the image should not be used for model training or evaluation.
#
# Note that, for single class annotation tasks, one does not need any UI but can instead simply drag-and-drop images into separate folder for the respective classes.
#
# See the [FAQ.md](..\FAQ.md) for a brief discussion on how to scrape images from the internet.
# In[1]:
# Ensure edits to libraries are loaded and plotting is shown in the notebook.
get_ipython().run_line_magic('reload_ext', 'autoreload')
get_ipython().run_line_magic('autoreload', '2')
get_ipython().run_line_magic('matplotlib', 'inline')
# In[2]:
import os, sys
sys.path.append("../")
from utils_ic.anno_utils import AnnotationWidget
from utils_ic.datasets import unzip_url, Urls
# Set parameters: location of the images to annotate, and path where to save the annotations. Here `unzip_url` is used to download example data if not already present, and set the path.
# In[3]:
IM_DIR = os.path.join((unzip_url(Urls.fridge_objects_path, exist_ok=True)), 'can')
ANNO_PATH = "cvbp_ic_annotation.txt"
print(f"Using images in directory: {IM_DIR}.")
# Start the UI. Check the "Allow multi-class labeling" box to allow for images to be annotated with multiple classes. When in doubt what the annotation for an image should be, or for any other reason (e.g. blur or over-exposure), mark an image as "EXCLUDE". All annotations are saved to (and loaded from) a pandas dataframe with path specified in `anno_path`.
#
# <center>
# <img src="media/anno_ui.jpg" style="width: 600px;"/>
# <i>Annotation UI example</i>
# </center>
# In[4]:
w_anno_ui = AnnotationWidget(
labels = ["can", "carton", "milk_bottle", "water_bottle"],
im_dir = IM_DIR,
anno_path = ANNO_PATH,
im_filenames = None #Set to None to annotate all images in IM_DIR
)
display(w_anno_ui.show())
# Below is an example how to create a fast.ai ImageList object using the ground truth annotations generated by the AnnotationWidget. Note that fast.ai does not support the exclude flag, hence we remove these images before calling fast.ai's `from_df()` and `label_from_df()` functions.
#
# ```python
# import pandas as pd
# from fastai.vision import ImageList,ImageDataBunch
#
# # Load annotation, discard excluded images, and convert to format fastai expects
# data = []
# with open(ANNO_PATH,'r') as f:
# for line in f.readlines()[1:]:
# vec = line.strip().split("\t")
# exclude = vec[1]=="True"
# if not exclude and len(vec)>2:
# data.append((vec[0], vec[2]))
#
# df = pd.DataFrame(data, columns = ["name", "label"])
# display(df)
#
# data = (ImageList.from_df(path=IM_DIR, df = df)
# .split_by_rand_pct(valid_pct=0.5)
# .label_from_df(cols='label', label_delim=','))
# ```

Просмотреть файл

@ -35,6 +35,8 @@ def notebooks():
"02_training_accuracy_vs_speed": os.path.join(
folder_notebooks, "02_training_accuracy_vs_speed.ipynb"
),
"10_image_annotation": os.path.join(
folder_notebooks, "10_image_annotation.ipynb"),
"11_exploring_hyperparameters": os.path.join(
folder_notebooks, "11_exploring_hyperparameters.ipynb"
),

Просмотреть файл

@ -0,0 +1,55 @@
import os
import numpy as np
from pathlib import Path
from PIL import Image
import pytest
from utils_ic.common import data_path, get_files_in_directory, ic_root_path, im_height, im_width, im_width_height
def test_ic_root_path():
s = ic_root_path()
assert isinstance(s, str) and s != ""
def test_data_path():
s = data_path()
assert isinstance(s, str) and s != ""
def test_im_width(tiny_ic_data_path):
im_path = Path(tiny_ic_data_path)/"can"/"1.jpg"
assert (
im_width(im_path) == 499
), "Expected image width of 499, but got {}".format(im_width(im_path))
im = np.zeros((100, 50))
assert im_width(im) == 50, "Expected image width of 50, but got ".format(
im_width(im)
)
def test_im_height(tiny_ic_data_path):
im_path = Path(tiny_ic_data_path)/"can"/"1.jpg"
assert (
im_height(im_path) == 665
), "Expected image height of 665, but got ".format(im_width(60))
im = np.zeros((100, 50))
assert (
im_height(im) == 100
), "Expected image height of 100, but got ".format(im_width(im))
def test_im_width_height(tiny_ic_data_path):
im_path = Path(tiny_ic_data_path)/"can"/"1.jpg"
w, h = im_width_height(im_path)
assert w == 499 and h == 665
im = np.zeros((100, 50))
w, h = im_width_height(im)
assert w == 50 and h == 100
def test_get_files_in_directory(tiny_ic_data_path):
im_dir = os.path.join(tiny_ic_data_path, "can")
assert len(get_files_in_directory(im_dir)) == 22
assert len(get_files_in_directory(im_dir, suffixes=[".jpg"])) == 22
assert len(get_files_in_directory(im_dir, suffixes=[".nonsense"])) == 0

Просмотреть файл

@ -1,16 +1,15 @@
# This test is based on the test suite implemented for Recommenders project
# https://github.com/Microsoft/Recommenders/tree/master/tests
import glob
import os
import glob
import papermill as pm
import shutil
# Unless manually modified, python3 should be
# the name of the current jupyter kernel
# that runs on the activated conda environment
KERNEL_NAME = "python3"
KERNEL_NAME = "cvbp"
OUTPUT_NOTEBOOK = "output.ipynb"
@ -52,6 +51,19 @@ def test_02_notebook_run(notebooks, tiny_ic_data_path):
)
def test_10_notebook_run(notebooks, tiny_ic_data_path):
notebook_path = notebooks["10_image_annotation"]
pm.execute_notebook(
notebook_path,
OUTPUT_NOTEBOOK,
parameters=dict(
PM_VERSION=pm.__version__,
IM_DIR=os.path.join(tiny_ic_data_path, "can"),
),
kernel_name=KERNEL_NAME,
)
def test_11_notebook_run(notebooks, tiny_ic_data_path):
notebook_path = notebooks["11_exploring_hyperparameters"]
pm.execute_notebook(
@ -104,3 +116,4 @@ def skip_test_deploy_1_notebook_run(notebooks, tiny_ic_data_path):
shutil.rmtree(os.path.join(os.getcwd(), "azureml-models"))
shutil.rmtree(os.path.join(os.getcwd(), "models"))
shutil.rmtree(os.path.join(os.getcwd(), "outputs"))

Просмотреть файл

@ -0,0 +1,265 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import os
from ipywidgets import widgets, Layout, IntSlider
import pandas as pd
from utils_ic.common import im_width, im_height, get_files_in_directory
class AnnotationWidget(object):
IM_WIDTH = 500 # pixels
def __init__(
self,
labels: list,
im_dir: str,
anno_path: str,
im_filenames: list = None,
):
"""Widget class to annotate images.
Args:
labels: List of abel names, e.g. ["bird", "car", "plane"].
im_dir: Directory containing the images to be annotated.
anno_path: path where to write annotations to, and (if exists) load annotations from.
im_fnames: List of image filenames. If set to None, then will auto-detect all images in the provided image directory.
"""
self.labels = labels
self.im_dir = im_dir
self.anno_path = anno_path
self.im_filenames = im_filenames
# Init
self.vis_image_index = 0
self.label_to_id = {s: i for i, s in enumerate(self.labels)}
if not im_filenames:
self.im_filenames = [os.path.basename(s) for s in get_files_in_directory(
im_dir,
suffixes=(
".jpg",
".jpeg",
".tif",
".tiff",
".gif",
".giff",
".png",
".bmp",
),
)]
assert len(self.im_filenames) > 0, f"Not a single image specified or found in directory {im_dir}."
# Initialize empty annotations and load previous annotations if file exist
self.annos = pd.DataFrame()
for im_filename in self.im_filenames:
if im_filename not in self.annos:
self.annos[im_filename] = pd.Series(
{"exclude": False, "labels": []}
)
if os.path.exists(self.anno_path):
print(f"Loading existing annotation from {self.anno_path}.")
with open(self.anno_path,'r') as f:
for line in f.readlines()[1:]:
vec = line.strip().split("\t")
im_filename = vec[0]
self.annos[im_filename].exclude = vec[1]=="True"
if len(vec)>2:
self.annos[im_filename].labels = vec[2].split(',')
# Create UI and "start" widget
self._create_ui()
def show(self):
return self.ui
def update_ui(self):
im_filename = self.im_filenames[self.vis_image_index]
im_path = os.path.join(self.im_dir, im_filename)
# Update the image and info
self.w_img.value = open(im_path, "rb").read()
self.w_filename.value = im_filename
self.w_path.value = self.im_dir
# Fix the width of the image widget and adjust the height
self.w_img.layout.height = (
f"{int(self.IM_WIDTH * (im_height(im_path)/im_width(im_path)))}px"
)
# Update annotations
self.exclude_widget.value = self.annos[im_filename].exclude
for w in self.label_widgets:
w.value = False
for label in self.annos[im_filename].labels:
label_id = self.label_to_id[label]
self.label_widgets[label_id].value = True
def _create_ui(self):
"""Create and initialize widgets"""
# ------------
# Callbacks + logic
# ------------
def skip_image():
"""Return true if image should be skipped, and false otherwise."""
# See if UI-checkbox to skip images is checked
if not self.w_skip_annotated.value:
return False
# Stop skipping if image index is out of bounds
if (
self.vis_image_index <= 0
or self.vis_image_index >= len(self.im_filenames) - 1
):
return False
# Skip if image has annotation
im_filename = self.im_filenames[self.vis_image_index]
labels = self.annos[im_filename].labels
exclude = self.annos[im_filename].exclude
if exclude or len(labels) > 0:
return True
return False
def button_pressed(obj):
"""Next / previous image button callback."""
# Find next/previous image. Variable step is -1 or +1 depending on which button was pressed.
step = int(obj.value)
self.vis_image_index += step
while skip_image():
self.vis_image_index += step
self.vis_image_index = min(
max(self.vis_image_index, 0), len(self.im_filenames) - 1
)
self.w_image_slider.value = self.vis_image_index
self.update_ui()
def slider_changed(obj):
"""Image slider callback.
Need to wrap in try statement to avoid errors when slider value is not a number.
"""
try:
self.vis_image_index = int(obj["new"]["value"])
self.update_ui()
except Exception:
pass
def anno_changed(obj):
"""Label checkbox callback.
Update annotation file and write to disk
"""
# Test if call is coming from the user having clicked on a checkbox to change its state,
# rather than a change of state when e.g. the checkbox value was updated programatically. This is a bit
# of hack, but necessary since widgets.Checkbox() does not support a on_click() callback or similar.
if "new" in obj and isinstance(obj["new"], dict) and len(obj["new"]) == 0:
# If single-label annotation then unset all checkboxes except the one which the user just clicked
if not self.w_multi_class.value:
for w in self.label_widgets:
if w.description != obj["owner"].description:
w.value = False
# Update annotation object
im_filename = self.im_filenames[self.vis_image_index]
self.annos[im_filename].labels = [
w.description for w in self.label_widgets if w.value
]
self.annos[im_filename].exclude = self.exclude_widget.value
# Write to disk as tab-separated file.
with open(self.anno_path,'w') as f:
f.write("{}\t{}\t{}\n".format("IM_FILENAME", "EXCLUDE", "LABELS"))
for k,v in self.annos.items():
if v.labels != [] or v.exclude:
f.write("{}\t{}\t{}\n".format(k, v.exclude, ",".join(v.labels)))
# ------------
# UI - image + controls (left side)
# ------------
w_next_image_button = widgets.Button(description="Next")
w_next_image_button.value = "1"
w_next_image_button.layout = Layout(width="80px")
w_next_image_button.on_click(button_pressed)
w_previous_image_button = widgets.Button(description="Previous")
w_previous_image_button.value = "-1"
w_previous_image_button.layout = Layout(width="80px")
w_previous_image_button.on_click(button_pressed)
self.w_filename = widgets.Text(
value="", description="Name:", layout=Layout(width="200px")
)
self.w_path = widgets.Text(
value="", description="Path:", layout=Layout(width="200px")
)
self.w_image_slider = IntSlider(
min=0,
max=len(self.im_filenames) - 1,
step=1,
value=self.vis_image_index,
continuous_update=False,
)
self.w_image_slider.observe(slider_changed)
self.w_img = widgets.Image()
self.w_img.layout.width = f"{self.IM_WIDTH}px"
w_header = widgets.HBox(
children=[
w_previous_image_button,
w_next_image_button,
self.w_image_slider,
self.w_filename,
self.w_path,
]
)
# ------------
# UI - info (right side)
# ------------
# Options widgets
self.w_skip_annotated = widgets.Checkbox(
value=False, description="Skip annotated images."
)
self.w_multi_class = widgets.Checkbox(
value=False, description="Allow multi-class labeling"
)
# Label checkboxes widgets
self.exclude_widget = widgets.Checkbox(
value=False, description="EXCLUDE IMAGE"
)
self.exclude_widget.observe(anno_changed)
self.label_widgets = [
widgets.Checkbox(value=False, description=label)
for label in self.labels
]
for label_widget in self.label_widgets:
label_widget.observe(anno_changed)
# Combine UIs into tab widget
w_info = widgets.VBox(
children=[
widgets.HTML(value="Options:"),
self.w_skip_annotated,
self.w_multi_class,
widgets.HTML(value="Annotations:"),
self.exclude_widget,
*self.label_widgets,
]
)
w_info.layout.padding = "20px"
self.ui = widgets.Tab(
children=[
widgets.VBox(
children=[
w_header,
widgets.HBox(children=[self.w_img, w_info]),
]
)
]
)
self.ui.set_title(0, "Annotator")
# Fill UI with content
self.update_ui()

Просмотреть файл

@ -1,5 +1,8 @@
import os
import numpy as np
from pathlib import Path
from PIL import Image
from typing import Union, Tuple, List
def ic_root_path() -> Path:
@ -12,3 +15,59 @@ def data_path() -> Path:
return os.path.realpath(
os.path.join(os.path.dirname(__file__), os.pardir, "data")
)
def im_width(input: Union[str, np.array]) -> int:
"""Returns the width of an image.
Args:
input: Image path or image as numpy array.
Return:
Image width.
"""
return im_width_height(input)[0]
def im_height(input: Union[str, np.array]) -> int:
"""Returns the height of an image.
Args:
input: Image path or image as numpy array.
Return:
Image height.
"""
return im_width_height(input)[1]
def im_width_height(input: Union[str, np.array]) -> Tuple[int, int]:
"""Returns the width and height of an image.
Args:
input: Image path or image as numpy array.
Return:
Tuple of ints (width,height).
"""
if isinstance(input, str) or isinstance(input, Path):
width, height = Image.open(
input
).size # this is fast since it does not load the full image
else:
width, height = (input.shape[1], input.shape[0])
return width, height
def get_files_in_directory(
directory: str, suffixes: List[str] = None
) -> List[str]:
"""Returns all filenames in a directory which optionally match one of multiple suffixes.
Args:
directory: directory to scan for files.
suffixes: only keep the filenames which ends with one of the suffixes (e.g. suffixes = [".jpg", ".png", ".gif"]).
Return:
List of filenames
"""
if not os.path.exists(directory):
raise Exception(f"Directory '{directory}' does not exist.")
filenames = [str(p) for p in Path(directory).iterdir() if p.is_file()]
if suffixes and suffixes != "":
filenames = [
s for s in filenames if s.lower().endswith(tuple(suffixes))
]
return filenames