Module deepposekit.annotate.gui.Annotator
Expand source code
# -*- coding: utf-8 -*-
# Copyright 2018-2019 Jacob M. Graving <jgraving@gmail.com>
#
# 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.
import numpy as np
import h5py
import os
from deepposekit.annotate.gui.GUI import GUI
from deepposekit.annotate.utils import hotkeys as keys
__all__ = ["Annotator"]
class Annotator(GUI):
"""
A GUI for annotating images.
------------------------------------------------------------
Keys | Action
------------------------------------------------------------
> +,- | Rescale the image
> Left mouse | Move active keypoint
> W, A, S, D | Move active keypoint
> space | Changes W,A,S,D mode (swaps between 1px or 10px)
> J, L | Load previous or next image
> <, > | Jump 10 images backward or forward
> I, K or |
tab, shift+tab | Switch active keypoint
> R | Mark frame as unannotated, or "reset"
> F | Mark frame as annotated or "finished"
> Esc, Q | Quit the Annotator GUI
------------------------------------------------------------
Note: Data is automatically saved when moving between frames.
Parameters
----------
datapath: str
Filepath of the HDF5 (.h5) file that contains the images to
be annotated.
dataset: str
Key name to access the images in the .h5 file.
skeleton: str
Filepath of the .csv or .xlsx file that has indexed information
on name of the keypoint (part, e.g. head), parent (the direct
connecting part, e.g. neck connects to head, parent is head),
and swap (swapping positions with a part when reflected).
See example file for more information.
scale: int/float, default 1
Scaling factor for the GUI (e.g. used in zooming).
text_scale: float
Scaling factor for the GUI font.
A text_scale of 1 works well for 1920x1080 (1080p) images
shuffle_colors: bool, default = True
Whether to shuffle the color order for drawing keypoints
refresh: int, default 100
Delay on receiving next keyboard input in milliseconds.
Attributes
----------
window_name: str
Name of the Annotation window when running program.
Set to be 'Annotation' unless otherwise changed.
n_images: int
Number of images in the .h5 file.
n_keypoints: int
Number of keypoints in the skeleton.
key: int
The key that is pressed on the keyboard.
image_idx: int
Index of a specific image in the .h5 file.
image: numpy.ndarray
One image accessed using image_idx.
Example
-------
>>> from deepposekit import Annotator
>>> app = Annotator('annotation.h5', 'images', 'skeleton.csv')
>>> app.run()
"""
def __init__(
self,
datapath,
dataset,
skeleton,
scale=1,
text_scale=0.15,
shuffle_colors=True,
refresh=100,
):
super(GUI, self).__init__()
self.window_name = "Annotation"
self.shuffle_colors = shuffle_colors
self._init_skeleton(skeleton)
if os.path.exists(datapath):
self._init_data(datapath, dataset)
else:
raise ValueError("datapath file or path does not exist")
self._init_gui(scale, text_scale, shuffle_colors, refresh)
def _init_data(self, datapath, dataset):
""" Initializes the images from the .h5 file (called in init).
Parameters
----------
datapath: str
Path of the .h5 file that contains the images to be annotated.
dataset: str
Key name to access the images in the .h5 file.
"""
if isinstance(datapath, str):
if datapath.endswith(".h5"):
self.datapath = datapath
else:
raise ValueError("datapath must be .h5 file")
else:
raise TypeError("datapath must be type `str`")
if isinstance(dataset, str):
self.dataset = dataset
else:
raise TypeError("dataset must be type `str`")
with h5py.File(self.datapath, "r+") as h5file:
self.n_images = h5file[self.dataset].shape[0]
# Check that all parts of the file exist
if "annotations" not in list(h5file.keys()):
empty_array = np.zeros((self.n_images, self.n_keypoints, 2))
h5file.create_dataset(
"annotations",
(self.n_images, self.n_keypoints, 2),
dtype=np.float64,
data=empty_array,
)
for idx in range(self.n_images):
h5file["annotations"][idx] = self.skeleton.loc[:, ["x", "y"]].values
if "annotated" not in list(h5file.keys()):
empty_array = np.zeros((self.n_images, self.n_keypoints), dtype=bool)
h5file.create_dataset(
"annotated",
(self.n_images, self.n_keypoints),
dtype=bool,
data=empty_array,
)
if "skeleton" not in list(h5file.keys()):
skeleton = self.skeleton[["tree", "swap_index"]].values
h5file.create_dataset(
"skeleton", skeleton.shape, dtype=np.int32, data=skeleton
)
# Unpack the images from the file
self.image_idx = np.sum(np.all(h5file["annotated"].value, axis=1)) - 1
self.image = h5file[self.dataset][self.image_idx]
self._check_grayscale()
self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx]
self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx]
def _save(self):
""" Saves an image.
Automatically called when moving to new images or invoked manually
using 'ctrl + s' keys.
"""
with h5py.File(self.datapath) as h5file:
h5file["annotations"][self.image_idx] = self.skeleton.loc[
:, ["x", "y"]
].values
h5file["annotated"][self.image_idx] = self.skeleton.loc[
:, "annotated"
].values
self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx]
self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx]
def _load(self):
""" Loads an image.
This method is called in _move_image_idx when moving to different
images. The image of specified image_idx will be loaded onto the GUI.
"""
with h5py.File(self.datapath) as h5file:
self.image = h5file[self.dataset][self.image_idx]
self._check_grayscale()
self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx]
self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx]
def _last_image(self):
""" Checks if image index is on the last index.
Helper method to check for the index of the last image in the h5 file.
Returns
-------
bool
Indicate if image_idx is the last index.
"""
return self.image_idx == self.n_images - 1
def _move_image_idx(self):
""" Move to different image.
Based on the key pressed, updates the image on the GUI.
The scheme is as follows:
------------------------------------------------------------
Keys | Action
------------------------------------------------------------
> <- , -> | Load previous or next image
> , , . | Jump 10 images backward or forward
------------------------------------------------------------
Every time the user moves from the image, the annotations
on the image is saved before loading the next image.
"""
# <- (left arrow) key
if self.key is keys.LEFTARROW:
self._save()
if self.image_idx == 0:
self.image_idx = self.n_images - 1
else:
self.image_idx -= 1
self._load()
# -> (right arrow) key
elif self.key is keys.RIGHTARROW:
self._save()
if self._last_image():
self.image_idx = 0
else:
self.image_idx += 1
self._load()
# . (period) key
elif self.key is keys.LESSTHAN:
self._save()
if self.image_idx - 10 < 0:
self.image_idx = self.n_images + self.image_idx - 10
else:
self.image_idx -= 10
self._load()
# , (comma) key
elif self.key is keys.GREATERTHAN:
self._save()
if self.image_idx + 10 > self.n_images - 1:
self.image_idx = self.image_idx + 10 - self.n_images
else:
self.image_idx += 10
self._load()
def _data(self):
""" Activates key bindings for annotated and save.
Creates additional key bindings for the program.
The bindings are as follows:
------------------------------------------------------------
Keys | Action
------------------------------------------------------------
> Ctrl-R | Mark frame as unannotated
> Ctrl-F | Mark frame as annotated
> Ctrl-S | Save
------------------------------------------------------------
"""
if self.key is keys.R:
self.skeleton["annotated"] = False
elif self.key is keys.F:
self.skeleton["annotated"] = True
elif self.key in [keys.Q, keys.ESC]:
self._save()
print("Saved")
def _hotkeys(self):
""" Activates all key bindings.
Enables all the key functionalities described at the
start of the file.
"""
if self.key != keys.NONE:
self._wasd()
self._move_idx()
self._move_image_idx()
self._zoom()
self._data()
self._update_canvas()
Classes
class Annotator (datapath, dataset, skeleton, scale=1, text_scale=0.15, shuffle_colors=True, refresh=100)
-
A GUI for annotating images.
Keys | Action
+,- | Rescale the image Left mouse | Move active keypoint W, A, S, D | Move active keypoint space | Changes W,A,S,D mode (swaps between 1px or 10px) J, L | Load previous or next image <, > | Jump 10 images backward or forward I, K or | tab, shift+tab | Switch active keypoint R | Mark frame as unannotated, or "reset" F | Mark frame as annotated or "finished" Esc, Q | Quit the Annotator GUI
Note: Data is automatically saved when moving between frames.
Parameters
datapath
:str
- Filepath of the HDF5 (.h5) file that contains the images to be annotated.
dataset
:str
- Key name to access the images in the .h5 file.
skeleton
:str
-
Filepath of the .csv or .xlsx file that has indexed information on name of the keypoint (part, e.g. head), parent (the direct connecting part, e.g. neck connects to head, parent is head), and swap (swapping positions with a part when reflected).
See example file for more information.
scale
:int
/float
, default1
- Scaling factor for the GUI (e.g. used in zooming).
text_scale
:float
- Scaling factor for the GUI font. A text_scale of 1 works well for 1920x1080 (1080p) images
shuffle_colors
:bool
, default =True
- Whether to shuffle the color order for drawing keypoints
refresh
:int
, default100
- Delay on receiving next keyboard input in milliseconds.
Attributes
window_name
:str
- Name of the Annotation window when running program. Set to be 'Annotation' unless otherwise changed.
n_images
:int
- Number of images in the .h5 file.
n_keypoints
:int
- Number of keypoints in the skeleton.
key
:int
- The key that is pressed on the keyboard.
image_idx
:int
- Index of a specific image in the .h5 file.
image
:numpy.ndarray
- One image accessed using image_idx.
Example
>>> from deepposekit import Annotator >>> app = Annotator('annotation.h5', 'images', 'skeleton.csv') >>> app.run()
A GUI for annotating or marking up image(s).
The GUI class works with a subclass to run a program that could be used to annotate or markup an image or a series of images. In order to extend the GUI class to make a subclass, a few things must be defined in the subclass.
Method |
> _hotkeys() | Activates all the hotkey bindings
Attribute |
window_name | Name of application window image_idx | Index of active image, important for multiple images n_images | Number of total images
See the Annotator.py or Skeleton.py for an example of how this is done
Attributes
scale
:float
- Scaling factor for the GUI (e.g. used in zooming).
text_scale
:float
- Scaling factor for the GUI font. A text_scale of 1 is good for 1920x1080 (1080p) images
refresh
:int
- Delay on receiving next keyboard input in milliseconds.
point
:numpy.ndarray
- The coordinates of the mouse on the GUI.
image
:numpy.ndarray
- One image accessed using image_idx.
canvas
:numpy.ndarray
- Canvas for the GUI itself.
skeleton
:pandas.DataFrame
- Store information from the skeleton data input.
idx
:int
- Index of the keypoint in question.
keypoint_idx
:numpy.ndarray
- Keeps track of the keypoints array.
n_keypoints
:int
- Total number of keypoints in an image.
text_locs
:list
- List of text locations.
key
:int
- The key that is pressed on the keyboard.
Expand source code
class Annotator(GUI): """ A GUI for annotating images. ------------------------------------------------------------ Keys | Action ------------------------------------------------------------ > +,- | Rescale the image > Left mouse | Move active keypoint > W, A, S, D | Move active keypoint > space | Changes W,A,S,D mode (swaps between 1px or 10px) > J, L | Load previous or next image > <, > | Jump 10 images backward or forward > I, K or | tab, shift+tab | Switch active keypoint > R | Mark frame as unannotated, or "reset" > F | Mark frame as annotated or "finished" > Esc, Q | Quit the Annotator GUI ------------------------------------------------------------ Note: Data is automatically saved when moving between frames. Parameters ---------- datapath: str Filepath of the HDF5 (.h5) file that contains the images to be annotated. dataset: str Key name to access the images in the .h5 file. skeleton: str Filepath of the .csv or .xlsx file that has indexed information on name of the keypoint (part, e.g. head), parent (the direct connecting part, e.g. neck connects to head, parent is head), and swap (swapping positions with a part when reflected). See example file for more information. scale: int/float, default 1 Scaling factor for the GUI (e.g. used in zooming). text_scale: float Scaling factor for the GUI font. A text_scale of 1 works well for 1920x1080 (1080p) images shuffle_colors: bool, default = True Whether to shuffle the color order for drawing keypoints refresh: int, default 100 Delay on receiving next keyboard input in milliseconds. Attributes ---------- window_name: str Name of the Annotation window when running program. Set to be 'Annotation' unless otherwise changed. n_images: int Number of images in the .h5 file. n_keypoints: int Number of keypoints in the skeleton. key: int The key that is pressed on the keyboard. image_idx: int Index of a specific image in the .h5 file. image: numpy.ndarray One image accessed using image_idx. Example ------- >>> from deepposekit import Annotator >>> app = Annotator('annotation.h5', 'images', 'skeleton.csv') >>> app.run() """ def __init__( self, datapath, dataset, skeleton, scale=1, text_scale=0.15, shuffle_colors=True, refresh=100, ): super(GUI, self).__init__() self.window_name = "Annotation" self.shuffle_colors = shuffle_colors self._init_skeleton(skeleton) if os.path.exists(datapath): self._init_data(datapath, dataset) else: raise ValueError("datapath file or path does not exist") self._init_gui(scale, text_scale, shuffle_colors, refresh) def _init_data(self, datapath, dataset): """ Initializes the images from the .h5 file (called in init). Parameters ---------- datapath: str Path of the .h5 file that contains the images to be annotated. dataset: str Key name to access the images in the .h5 file. """ if isinstance(datapath, str): if datapath.endswith(".h5"): self.datapath = datapath else: raise ValueError("datapath must be .h5 file") else: raise TypeError("datapath must be type `str`") if isinstance(dataset, str): self.dataset = dataset else: raise TypeError("dataset must be type `str`") with h5py.File(self.datapath, "r+") as h5file: self.n_images = h5file[self.dataset].shape[0] # Check that all parts of the file exist if "annotations" not in list(h5file.keys()): empty_array = np.zeros((self.n_images, self.n_keypoints, 2)) h5file.create_dataset( "annotations", (self.n_images, self.n_keypoints, 2), dtype=np.float64, data=empty_array, ) for idx in range(self.n_images): h5file["annotations"][idx] = self.skeleton.loc[:, ["x", "y"]].values if "annotated" not in list(h5file.keys()): empty_array = np.zeros((self.n_images, self.n_keypoints), dtype=bool) h5file.create_dataset( "annotated", (self.n_images, self.n_keypoints), dtype=bool, data=empty_array, ) if "skeleton" not in list(h5file.keys()): skeleton = self.skeleton[["tree", "swap_index"]].values h5file.create_dataset( "skeleton", skeleton.shape, dtype=np.int32, data=skeleton ) # Unpack the images from the file self.image_idx = np.sum(np.all(h5file["annotated"].value, axis=1)) - 1 self.image = h5file[self.dataset][self.image_idx] self._check_grayscale() self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx] self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx] def _save(self): """ Saves an image. Automatically called when moving to new images or invoked manually using 'ctrl + s' keys. """ with h5py.File(self.datapath) as h5file: h5file["annotations"][self.image_idx] = self.skeleton.loc[ :, ["x", "y"] ].values h5file["annotated"][self.image_idx] = self.skeleton.loc[ :, "annotated" ].values self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx] self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx] def _load(self): """ Loads an image. This method is called in _move_image_idx when moving to different images. The image of specified image_idx will be loaded onto the GUI. """ with h5py.File(self.datapath) as h5file: self.image = h5file[self.dataset][self.image_idx] self._check_grayscale() self.skeleton.loc[:, ["x", "y"]] = h5file["annotations"][self.image_idx] self.skeleton.loc[:, "annotated"] = h5file["annotated"][self.image_idx] def _last_image(self): """ Checks if image index is on the last index. Helper method to check for the index of the last image in the h5 file. Returns ------- bool Indicate if image_idx is the last index. """ return self.image_idx == self.n_images - 1 def _move_image_idx(self): """ Move to different image. Based on the key pressed, updates the image on the GUI. The scheme is as follows: ------------------------------------------------------------ Keys | Action ------------------------------------------------------------ > <- , -> | Load previous or next image > , , . | Jump 10 images backward or forward ------------------------------------------------------------ Every time the user moves from the image, the annotations on the image is saved before loading the next image. """ # <- (left arrow) key if self.key is keys.LEFTARROW: self._save() if self.image_idx == 0: self.image_idx = self.n_images - 1 else: self.image_idx -= 1 self._load() # -> (right arrow) key elif self.key is keys.RIGHTARROW: self._save() if self._last_image(): self.image_idx = 0 else: self.image_idx += 1 self._load() # . (period) key elif self.key is keys.LESSTHAN: self._save() if self.image_idx - 10 < 0: self.image_idx = self.n_images + self.image_idx - 10 else: self.image_idx -= 10 self._load() # , (comma) key elif self.key is keys.GREATERTHAN: self._save() if self.image_idx + 10 > self.n_images - 1: self.image_idx = self.image_idx + 10 - self.n_images else: self.image_idx += 10 self._load() def _data(self): """ Activates key bindings for annotated and save. Creates additional key bindings for the program. The bindings are as follows: ------------------------------------------------------------ Keys | Action ------------------------------------------------------------ > Ctrl-R | Mark frame as unannotated > Ctrl-F | Mark frame as annotated > Ctrl-S | Save ------------------------------------------------------------ """ if self.key is keys.R: self.skeleton["annotated"] = False elif self.key is keys.F: self.skeleton["annotated"] = True elif self.key in [keys.Q, keys.ESC]: self._save() print("Saved") def _hotkeys(self): """ Activates all key bindings. Enables all the key functionalities described at the start of the file. """ if self.key != keys.NONE: self._wasd() self._move_idx() self._move_image_idx() self._zoom() self._data() self._update_canvas()
Ancestors
Inherited members