Skip to content

Commit f8d2572

Browse files
committed
Merge branch 'release' into 'github-master'
January Release See merge request products/ipyannotator/ipyannotator!31
2 parents b283458 + 5a6a035 commit f8d2572

25 files changed

Lines changed: 1016 additions & 728 deletions

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ RUN git clone git://github.com/yyuu/pyenv.git .pyenv
4545

4646
RUN pyenv install 3.7.3 -f && pyenv global 3.7.3
4747

48-
ENV POETRY_VERSION=1.0.5 \
48+
ENV POETRY_VERSION=1.1.0 \
4949
PIP_DISABLE_PIP_VERSION_CHECK=on \
5050
POETRY_NO_INTERACTION=1 \
5151
POETRY_VIRTUALENVS_CREATE=false \

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,39 @@ ipywidgets = "^7.5.1"
3838
```
3939

4040

41-
## Run ipyannotator as stand alone web app using voila
41+
## Run ipyannotator as stand-alone web app using voila
4242

43+
Dependency resolution fails if `nbdev` and `voila` libraries are both listed in the same `pyproject.toml`. This should be fixed in the next major release of `nbdev` lib.
4344

44-
`voila nbs/09_viola_example.ipynb --enable_nbextensions=True`
45+
The easiest workaround atm:
46+
- install `ipyannotator` without dev dependencies
47+
- manually install `voila` into the same dev environment
4548

49+
Using `poetry`:
50+
51+
install:
52+
```shell
53+
cd {project_root}
54+
poetry install --no-dev
55+
poetry run pip install voila
56+
```
57+
and run simple ipyannotator standalone example:
58+
```shell
59+
poetry run voila nbs/09_viola_example.ipynb --enable_nbextensions=True
60+
```
61+
62+
63+
Same with `pip`:
64+
65+
```shell
66+
cd {project_root}
67+
68+
pip install .
69+
pip install voila
70+
71+
voila nbs/09_viola_example.ipynb --enable_nbextensions=True
72+
```
73+
4674

4775
# Documentation
4876

dev_notes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,12 @@ For clean (re)install:
158158

159159
`nbdime extensions --enable [--sys-prefix/--user/--system]`
160160

161+
<HR>
162+
163+
To automatically install `voila` together with `nbdev` an old revision can be used, however this is not included in current `pyproject.toml` file to avoid a very time consuming `git clone`, which slows down the poetry dependency resolution dramatically.
164+
165+
```toml
166+
voila = { git = "https://github.com/voila-dashboards/voila.git", rev = "e23fcca926584a5aa837c3354804aa2d761edda3" }
167+
```
168+
169+
Manual workaround with viola app described in `README.md`.

ipyannotator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.1"
1+
__version__ = "0.3.0"

ipyannotator/bbox_annotator.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/04_bbox_annotator.ipynb (unless otherwise specified).
22

3-
__all__ = ['BBoxAnnotatorGUI', 'BBoxAnnotatorLogic', 'BBoxAnnotator']
3+
__all__ = ['BBoxAnnotator']
44

5-
# Cell
5+
# Internal Cell
66

77
import os
88
import json
@@ -18,7 +18,7 @@
1818
from .navi_widget import Navi
1919
from .storage import setup_project_paths, get_image_list_from_folder, AnnotationStorage
2020

21-
# Cell
21+
# Internal Cell
2222

2323
class BBoxAnnotatorGUI(AppLayout):
2424

@@ -45,7 +45,7 @@ def __init__(self, canvas_size=(505, 50)):
4545
def on_client_ready(self, callback):
4646
self._image_box.observe_client_ready(callback)
4747

48-
# Cell
48+
# Internal Cell
4949

5050
class BBoxAnnotatorLogic(HasTraits):
5151
index = Int(0)
@@ -96,6 +96,14 @@ def __get_name_by_index(self, idx):
9696
# Cell
9797

9898
class BBoxAnnotator(BBoxAnnotatorGUI):
99+
"""
100+
Represents bounding box annotator.
101+
102+
Gives an ability to itarate through image dataset,
103+
draw 2D bounding box annotations for object detection and localization,
104+
export final annotations in json format
105+
106+
"""
99107
debug_output = Output()
100108

101109
def __init__(self, project_path, canvas_size=(200, 400), image_dir='pics'):

ipyannotator/bbox_canvas.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/01_bbox_canvas.ipynb (unless otherwise specified).
22

3-
__all__ = ['draw_bg', 'draw_bounding_box', 'get_image_size', 'draw_img', 'points2bbox_coords', 'BBoxCanvas']
3+
__all__ = ['BBoxCanvas']
44

5-
# Cell
5+
# Internal Cell
66
import numpy as np
77
import traitlets
88
import ipywidgets as widgets
@@ -11,14 +11,14 @@
1111
from ipycanvas import MultiCanvas, hold_canvas
1212
from ipywidgets import Image, Label, Layout, HBox, VBox, Output
1313

14-
# Cell
14+
# Internal Cell
1515

1616
def draw_bg(canvas, color='rgb(236,240,241)'):
1717
with hold_canvas(canvas):
1818
canvas.fill_style = color
1919
canvas.fill_rect(0, 0, canvas.size[0], canvas.size[1])
2020

21-
# Cell
21+
# Internal Cell
2222

2323
def draw_bounding_box(canvas, coords, color='white', line_width=None,
2424
border_ratio=2, clear=False):
@@ -41,15 +41,15 @@ def draw_bounding_box(canvas, coords, color='white', line_width=None,
4141
canvas.stroke_rect(pos_x + gap, pos_y + gap, rect_x - 2 * gap,
4242
rect_y - 2 * gap)
4343

44-
# Cell
44+
# Internal Cell
4545
from PIL import Image as pilImage
4646

4747
# can we do this without reading image?
4848
def get_image_size(path):
4949
pil_im = pilImage.open(path)
5050
return pil_im.width, pil_im.height
5151

52-
# Cell
52+
# Internal Cell
5353

5454
def draw_img(canvas, file, clear=False):
5555
# draws resized image on canvas and returns scale used
@@ -77,7 +77,7 @@ def draw_img(canvas, file, clear=False):
7777
height=height_img * min(1, scale))
7878
return scale
7979

80-
# Cell
80+
# Internal Cell
8181

8282
def points2bbox_coords(start_x, start_y, end_x, end_y):
8383
min_x, max_x = sorted((start_x, end_x))
@@ -88,6 +88,11 @@ def points2bbox_coords(start_x, start_y, end_x, end_y):
8888
# Cell
8989

9090
class BBoxCanvas(HBox, traitlets.HasTraits):
91+
"""
92+
Represents canvas holding image and bbox ontop.
93+
Gives user an ability to draw a bbox with mouse.
94+
95+
"""
9196
debug_output = widgets.Output(layout={'border': '1px solid black'})
9297
image_path = traitlets.Unicode()
9398
bbox_coords = traitlets.Dict()
@@ -104,6 +109,8 @@ def __init__(self, width, height):
104109
self._bg_layer = 0
105110
self._image_layer = 1
106111
self._box_layer = 2
112+
# do not stick bbox to borders
113+
self.padding = 2
107114

108115
# Define each of the children...
109116
self._image = Image(layout=Layout(display='flex',
@@ -122,40 +129,79 @@ def __init__(self, width, height):
122129
self._multi_canvas[self._box_layer].on_mouse_down(self._start_drawing)
123130
self._multi_canvas[self._box_layer].on_mouse_up(self._stop_drawing)
124131

132+
125133
@debug_output.capture(clear_output=False)
126134
def _update_pos(self, x, y):
127135
if self.is_drawing:
128136
self._canvas_bbox_coords = points2bbox_coords(*self._start_point, x, y)
137+
# bbox should not cross the canvas border:
138+
if self._invalid_coords(x, y):
139+
print(' !! Out of canvas border !!')
140+
self._stop_drawing(x, y)
141+
142+
def _invalid_coords(self, x, y):
143+
return (self._canvas_bbox_coords["x"] + self._canvas_bbox_coords["width"] > self._multi_canvas.width - self.padding or
144+
self._canvas_bbox_coords["y"] + self._canvas_bbox_coords["height"] > self._multi_canvas.height - self.padding or
145+
self._canvas_bbox_coords["x"] < self.padding or
146+
self._canvas_bbox_coords["y"] < self.padding)
147+
129148

130149
@debug_output.capture(clear_output=True)
131150
def _start_drawing(self, x, y):
151+
# print("-> START DRAWING")
132152
self._start_point = (x, y)
133153
self.is_drawing = True
154+
# print("<- START DRAWING")
134155

135156
@debug_output.capture(clear_output=False)
136157
def _stop_drawing(self, x, y):
158+
# print("-> STOP DRAWING")
137159
self.is_drawing = False
138-
self.bbox_coords = {k: v / self._image_scale for k, v in self._canvas_bbox_coords.items()}
160+
161+
# if something is drawn
162+
if self._canvas_bbox_coords:
163+
# if bbox is not human visible, clean:
164+
if (self._canvas_bbox_coords['width'] < 10 or
165+
self._canvas_bbox_coords['height'] < 10):
166+
self._canvas_bbox_coords = {}
167+
print(" !! too small bbox drawn !!")
168+
else: # otherwise, save bbox values to backend
169+
self.bbox_coords = dict({ k: v / self._image_scale for k, v in self._canvas_bbox_coords.items() })
170+
# print("<- STOP DRAWING")
171+
139172

140173
@traitlets.observe('bbox_coords')
141174
def _update_canvas_bbox_coords(self, change):
142-
self._canvas_bbox_coords = {k: v * self._image_scale for k, v in self.bbox_coords.items()}
175+
# print('-> Observe bbox_coords: ', change)
176+
177+
if change['new'] == self._canvas_bbox_coords: # change event from gui, do nothing
178+
print('-> GUI')
179+
else: # recalculate canvas coordinates as bbox was set by backend:
180+
self._canvas_bbox_coords = {k: v * self._image_scale for k, v in self.bbox_coords.items()}
181+
print('-> Backend')
182+
# print('<- Observe bbox_coords')
183+
143184

144185
@traitlets.observe('_canvas_bbox_coords')
145186
def _draw_bbox(self, change):
187+
# print('-> Observe canvas_coords: ', change)
146188
if not self._canvas_bbox_coords:
147189
self._clear_bbox()
190+
self.bbox_coords = {}
148191
return
149192
coords = [self._canvas_bbox_coords['x'],
150193
self._canvas_bbox_coords['y'],
151194
self._canvas_bbox_coords['width'],
152195
self._canvas_bbox_coords['height']]
153196
draw_bounding_box(self._multi_canvas[self._box_layer], coords,
154197
color='white', border_ratio=2, clear=True)
198+
# print('<- Observe canvas_coords')
199+
155200

156201
def _clear_bbox(self):
157202
self._multi_canvas[self._box_layer].clear()
158203

204+
159205
@traitlets.observe('image_path')
160206
def _draw_image(self, image):
161207
self._image_scale = draw_img(self._multi_canvas[self._image_layer], self.image_path, clear=True)

ipyannotator/capture_annotator.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/06_capture_annotator.ipynb (unless otherwise specified).
22

3-
__all__ = ['CaptureGrid', 'CaptureAnnotatorGUI', 'CaptureAnnotatorLogic', 'CaptureAnnotator']
3+
__all__ = ['CaptureGrid', 'CaptureAnnotator']
44

5-
# Cell
5+
# Internal Cell
66

77
from functools import partial
88
import math
@@ -22,6 +22,10 @@
2222
# Cell
2323

2424
class CaptureGrid(GridBox, HasTraits):
25+
"""
26+
Represents grid of `ImageButtons` with state.
27+
28+
"""
2529
debug_output = Output(layout={'border': '1px solid black'})
2630
current_state = Dict()
2731

@@ -92,7 +96,7 @@ def register_on_click(self):
9296
l.reset_callbacks()
9397
l.on_click(partial(self.callback, name=l.name))
9498

95-
# Cell
99+
# Internal Cell
96100

97101
class CaptureAnnotatorGUI(AppLayout):
98102
def __init__(self, image_width=150, image_height=150,
@@ -137,7 +141,7 @@ def __init__(self, image_width=150, image_height=150,
137141
pane_heights=(1, 4, 1))
138142

139143

140-
# Cell
144+
# Internal Cell
141145

142146
class CaptureAnnotatorLogic(HasTraits):
143147
debug_output = Output(layout={'border': '1px solid black'})
@@ -223,6 +227,14 @@ def _select_none(self, change=None):
223227

224228

225229
class CaptureAnnotator(CaptureAnnotatorGUI):
230+
"""
231+
Represents capture annotator.
232+
233+
Gives an ability to itarate through image dataset,
234+
select images of same class,
235+
export final annotations in json format
236+
237+
"""
226238

227239
def __init__(self, project_path, image_dir='pics', image_width=150, image_height=150,
228240
n_rows=3, n_cols=3, question=None, filter_files=None):

ipyannotator/common/__init__.py

Whitespace-only changes.

ipyannotator/common/settings.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)