Skip to content

Commit

Permalink
Merge pull request #480 from Living-with-machines/dev_annotate
Browse files Browse the repository at this point in the history
Dev annotate
  • Loading branch information
rwood-97 authored Aug 9, 2024
2 parents 5ba6c47 + d9aa85d commit 3c85f6b
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 86 deletions.
39 changes: 39 additions & 0 deletions docs/source/using-mapreader/step-by-step-guide/3-annotate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ In the above examples, the following parameters are also specified:
Other arguments that you may want to be aware of when initializing the ``Annotator`` instance include:

- ``show_context``: Whether to show a context image in the annotation interface (default: ``False``).
- ``border``: Whether to show a border around the central patch when showing context (default: ``False``).
- ``surrounding``: How many surrounding patches to show in the context image (default: ``1``).
- ``sortby``: The name of the column to use to sort the patch Dataframe (e.g. "mean_pixel_R" to sort by red pixel intensities).
- ``ascending``: A boolean indicating whether to sort in ascending or descending order (default: ``True``).
Expand Down Expand Up @@ -137,6 +138,30 @@ Or, equivalently, :
.. note:: Passing the ``show_context`` argument when calling the ``annotate()`` method overrides the ``show_context`` argument passed when initializing the ``Annotator`` instance.

If you have set ``show_context=True``, you can also choose to show a border around the central patch using the ``border`` argument:

.. code-block:: python
# EXAMPLE
annotator = Annotator(
patch_df="./patch_df.csv",
parent_df="./parent_df.csv",
annotations_dir="./annotations",
task_name="railspace",
labels=["no_railspace", "railspace"],
username="rosie",
show_context=True,
border=True,
)
annotator.annotate()
or, equivalently, :

.. code-block:: python
annotator.annotate(show_context=True, border=True)
By default, your ``Annotator`` will show one surrounding patch in the context image.
You can change this by passing the ``surrounding`` argument when initializing the ``Annotator`` instance and/or when calling the ``annotate`` method.

Expand Down Expand Up @@ -214,6 +239,20 @@ This will only show you patches that have been predicted to be "railspace".

You can filter for any column in your patch DataFrame, and you can filter for multiple values by passing multiple key-value pairs as your ``filter_for`` dictionary.

Showing additional information about your patches in the annotation interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you would like to show additional information about your patches in the annotation interface, you can pass the names of the columns you would like to show using the ``show_vals`` argument when calling the ``annotate()`` method.

e.g. to show the ``"mean_pixel"`` and ``"std_pixel"`` columns in the annotation interface, you should pass these column names as a list to the ``show_vals`` argument:

.. code-block:: python
annotator.annotate(show_vals=["mean_pixel", "std_pixel"])
The values in these columns will then be shown below the patch when you are annotating.
This can help you get an idea of which mean pixel values you might want to filter for or use as "min_values" or "max_values" arguments when annotating.

.. _Save_annotations:

Save your annotations
Expand Down
80 changes: 62 additions & 18 deletions mapreader/annotate/annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class Annotator:
Name of the column in which labels are stored in patch DataFrame, by default "label"
show_context : bool, optional
Whether to show context when loading patches, by default False
border : bool, optional
Whether to add a border around the central patch when showing context, by default False
auto_save : bool, optional
Whether to automatically save annotations, by default True
delimiter : str, optional
Expand Down Expand Up @@ -108,6 +110,7 @@ def __init__(
patch_paths_col: str = "image_path",
label_col: str = "label",
show_context: bool = False,
border: bool = False,
auto_save: bool = True,
delimiter: str = ",",
sortby: str | None = None,
Expand Down Expand Up @@ -255,6 +258,7 @@ def __init__(
self.patch_paths_col = patch_paths_col
self.annotations_file = annotations_file
self.show_context = show_context
self.border = border
self.auto_save = auto_save
self.username = username
self.task_name = task_name
Expand Down Expand Up @@ -452,7 +456,7 @@ def _setup_box(self) -> None:

self.navbox = widgets.VBox([widgets.HBox([prev_btn, next_btn])])

def get_queue(
def _get_queue(
self, as_type: str | None = "list"
) -> list[int] | (pd.Index | pd.Series):
"""
Expand Down Expand Up @@ -520,7 +524,7 @@ def check_eligibility(row):
return indices
return queue_df

def get_context(self):
def _get_context(self):
"""
Provides the surrounding context for the patch to be annotated.
Expand All @@ -531,15 +535,21 @@ def get_context(self):
context.
"""

def get_path(image_path, dim=True):
def get_square(image_path, dim=True, border=False):
# Resize the image
im = Image.open(image_path)

# Dim the image
if dim in [True, "True"]:
if dim in ["True", True]:
im_array = np.array(im)
im_array = 256 - (256 - im_array) * 0.4 # lighten image
im = Image.fromarray(im_array.astype(np.uint8))

if border in ["True", True] and self.border:
w, h = im.size
im = ImageOps.expand(im, border=2, fill="red")
im = im.resize((w, h))

return im

def get_empty_square(patch_size: tuple[int, int]):
Expand Down Expand Up @@ -593,22 +603,27 @@ def get_empty_square(patch_size: tuple[int, int]):

# derive ids from items
ids = [x.index[0] if len(x.index) == 1 else None for x in items]
ids = [x != ix for x in ids]
# list of booleans, True if not the current patch, False if the current patch
# used for dimming the surrounding patches and adding a border to the current patch
dim_bools = [x != ix for x in ids]
border_bools = [x == ix for x in ids]

# derive images from items
image_paths = [
x.at[x.index[0], "image_path"] if len(x.index) == 1 else None for x in items
]

# zip them
image_list = list(zip(image_paths, ids))
image_list = list(zip(image_paths, dim_bools, border_bools))

# split them into rows
per_row = len(deltas)
images = [
[
get_path(x[0], dim=x[1]) if x[0] else get_empty_square((width, height))
for x in lst
get_square(image_path, dim=dim, border=border)
if image_path
else get_empty_square((width, height))
for image_path, dim, border in lst
]
for lst in array_split(image_list, per_row)
]
Expand Down Expand Up @@ -641,13 +656,15 @@ def get_empty_square(patch_size: tuple[int, int]):
def annotate(
self,
show_context: bool | None = None,
border: bool | None = None,
sortby: str | None = None,
ascending: bool | None = None,
min_values: dict | None = None,
max_values: dict | None = None,
surrounding: int | None = None,
resize_to: int | None = None,
max_size: int | None = None,
show_vals: list[str] | None = None,
) -> None:
"""Annotate at the patch-level of the current patch.
Renders the annotation interface for the first image.
Expand All @@ -657,6 +674,9 @@ def annotate(
show_context : bool or None, optional
Whether or not to display the surrounding context for each image.
Default is None.
border : bool or None, optional
Whether or not to display a border around the image (when using `show_context`).
Default is None.
sortby : str or None, optional
Name of the column to use to sort the patch DataFrame, by default None.
Default sort order is ``ascending=True``. Pass ``ascending=False`` keyword argument to sort in descending order.
Expand All @@ -677,12 +697,18 @@ def annotate(
max_size : int or None, optional
The size in pixels for the longest side to which constrain each
patch image. Default: 100.
resize_to : int or None, optional
The size in pixels for the longest side to which resize each patch image. Default: None.
show_vals : list[str] or None, optional
List of column names to show in the display. By default, None.
Notes
-----
This method is a wrapper for the
:meth:`~.annotate.annotator.Annotate._annotate` method.
"""
if border is not None:
self.border = border
if sortby is not None:
self._sortby = sortby
if ascending is not None:
Expand All @@ -693,8 +719,10 @@ def annotate(
if max_values is not None:
self._max_values = max_values

self.show_vals = show_vals

# re-set up queue using new min/max values
self._queue = self.get_queue()
self._queue = self._get_queue()

self._annotate(
show_context=show_context,
Expand Down Expand Up @@ -743,7 +771,7 @@ def _annotate(
self.max_size = max_size

# re-set up queue
self._queue = self.get_queue()
self._queue = self._get_queue()

if self._filter_for is not None:
print(f"[INFO] Filtering for: {self._filter_for}")
Expand All @@ -767,7 +795,7 @@ def _next_example(self, *_) -> tuple[int, int, str]:
Previous index, current index, and path of the current image.
"""
if self.current_index == len(self._queue):
self.render_complete()
self._render_complete()
return

self.previous_index = self.current_index
Expand All @@ -777,7 +805,7 @@ def _next_example(self, *_) -> tuple[int, int, str]:

img_path = self.patch_df.at[ix, self.patch_paths_col]

self.render()
self._render()
return self.previous_index, self.current_index, img_path

def _prev_example(self, *_) -> tuple[int, int, str]:
Expand All @@ -790,7 +818,7 @@ def _prev_example(self, *_) -> tuple[int, int, str]:
Previous index, current index, and path of the current image.
"""
if self.current_index == len(self._queue):
self.render_complete()
self._render_complete()
return

if self.current_index > 0:
Expand All @@ -801,10 +829,10 @@ def _prev_example(self, *_) -> tuple[int, int, str]:

img_path = self.patch_df.at[ix, self.patch_paths_col]

self.render()
self._render()
return self.previous_index, self.current_index, img_path

def render(self) -> None:
def _render(self) -> None:
"""
Displays the image at the current index in the annotation interface.
Expand All @@ -817,7 +845,7 @@ def render(self) -> None:
"""
# Check whether we have reached the end
if self.current_index >= len(self) - 1:
self.render_complete()
self._render_complete()
return

ix = self._queue[self.current_index]
Expand All @@ -841,7 +869,7 @@ def render(self) -> None:
clear_output(wait=True)
image = self.get_patch_image(ix)
if self.show_context:
context = self.get_context()
context = self._get_context()
self._context_image = context
display(context.convert("RGB"))
else:
Expand All @@ -852,6 +880,22 @@ def render(self) -> None:
text = f'<p><a href="{url}" target="_blank">Click to see entire map.</a></p>'
add_ins += [widgets.HTML(text)]

if self.show_vals:
patch_info = []
for col in self.show_vals:
if col in self.patch_df.columns:
val = self.patch_df.at[ix, col]
if isinstance(val, float):
val = f"{val:.4g}"
patch_info.append(f"<b>{col}</b>: {val}")
add_ins += [
widgets.HTML(
'<p style="text-align: center">'
+ "<br>".join(patch_info)
+ "</p>"
)
]

value = self.current_index + 1 if self.current_index else 1
description = f"{value} / {len(self._queue)}"
add_ins += [
Expand Down Expand Up @@ -983,7 +1027,7 @@ def filtered(self) -> pd.DataFrame:
_filter = ~self.patch_df[self.label_col].isna()
return self.patch_df[_filter]

def render_complete(self):
def _render_complete(self):
"""
Renders the completion message once all images have been annotated.
Expand Down
Loading

0 comments on commit 3c85f6b

Please sign in to comment.