From e93692f09e978efb9f420e73a4db5aa11c75a75b Mon Sep 17 00:00:00 2001 From: = Date: Fri, 8 Oct 2021 20:21:34 +0200 Subject: [PATCH] Added image resize on runtime feature. --- ipyannotator/bbox_canvas.py | 16 +++-- ipyannotator/capture_annotator.py | 3 + ipyannotator/im2im_annotator.py | 32 +++++++++- ipyannotator/navi_widget.py | 35 ++++++++--- nbs/01_bbox_canvas.ipynb | 33 +++++++---- nbs/01b_tutorial_image_classification.ipynb | 2 +- nbs/02_navi_widget.ipynb | 39 +++++++++--- nbs/06_capture_annotator.ipynb | 3 + nbs/07_im2im_annotator.ipynb | 66 +++++++++++++++++---- 9 files changed, 181 insertions(+), 48 deletions(-) diff --git a/ipyannotator/bbox_canvas.py b/ipyannotator/bbox_canvas.py index 62d7cbf..568c46a 100644 --- a/ipyannotator/bbox_canvas.py +++ b/ipyannotator/bbox_canvas.py @@ -51,15 +51,20 @@ def get_image_size(path): # Internal Cell -def draw_img(canvas, file, clear=False): - # draws resized image on canvas and returns scale used +def draw_img(canvas, file, clear=False, canvas_size=None, rescale=1.0): + # draws an image on canvas, a specific size can be passed to scale image with hold_canvas(canvas): if clear: canvas.clear() sprite1 = Image.from_file(file) - width_canvas, height_canvas = canvas.width, canvas.height + if canvas_size: + width_canvas, height_canvas = canvas_size + # if no specific size is passed, use current canvas size + else: + width_canvas, height_canvas = canvas.width, canvas.height + width_img, height_img = get_image_size(file) ratio_canvas = float(width_canvas) / height_canvas @@ -73,8 +78,8 @@ def draw_img(canvas, file, clear=False): scale = height_canvas / height_img canvas.draw_image(sprite1, 0, 0, - width=width_img * min(1, scale), - height=height_img * min(1, scale)) + width=(width_img * min(1, scale)) * rescale, + height=(height_img * min(1, scale)) * rescale) return scale # Internal Cell @@ -202,7 +207,6 @@ def _draw_bbox(self, change): def _clear_bbox(self): self._multi_canvas[self._box_layer].clear() - @traitlets.observe('image_path') def _draw_image(self, image): self._image_scale = draw_img(self._multi_canvas[self._image_layer], self.image_path, clear=True) diff --git a/ipyannotator/capture_annotator.py b/ipyannotator/capture_annotator.py index 6483392..2205eaa 100644 --- a/ipyannotator/capture_annotator.py +++ b/ipyannotator/capture_annotator.py @@ -120,6 +120,9 @@ def __init__(self, image_width=150, image_height=150, indent=False, layout=Layout(width='100px')) + self._none_checkbox.add_class("none-checkbox-class") + display(HTML("")) + self._controls_box = HBox([self._navi, self._save_btn, self._none_checkbox], layout=Layout(display='flex', justify_content='center', flex_flow='wrap', align_items='center')) diff --git a/ipyannotator/im2im_annotator.py b/ipyannotator/im2im_annotator.py index f9dc067..c2fdfb0 100644 --- a/ipyannotator/im2im_annotator.py +++ b/ipyannotator/im2im_annotator.py @@ -32,22 +32,39 @@ class ImCanvas(HBox, HasTraits): image_path = Unicode() _image_scale = Float() + _image_rescale = Float(1.0) def __init__(self, width=150, height=150): self._canvas = Canvas(width=width, height=height) + self._initial_canvas_size = self._canvas.size super().__init__([self._canvas]) + def _draw_image(self, canvas_size=None): + self._image_scale = draw_img(self._canvas, self.image_path, clear=True, + canvas_size=canvas_size, rescale=self._image_rescale) + @observe('image_path') - def _draw_image(self, change): - self._image_scale = draw_img(self._canvas, self.image_path, clear=True) + def _call_draw_image(self, change): + self._draw_image(self._initial_canvas_size) # Add value as a read-only property @property def image_scale(self): return self._image_scale + @observe('_image_rescale') + def _redraw_image(self, change): + # Resize canvas + new_width = self._initial_canvas_size[0] * self._image_rescale + new_height = self._initial_canvas_size[1] * self._image_rescale + self._canvas.size = (new_width, new_height) + + # As draw_image method uses canvas current size, we pass + # as parameter the initial size of canvas (before rescaling it too) + self._draw_image(self._initial_canvas_size) + def _clear_image(self): self._canvas.clear() @@ -81,7 +98,7 @@ def __init__(self, im_width=300, im_height=300, self._image = ImCanvas(width=im_width, height=im_height) - self._navi = Navi() + self._navi = Navi(disable_resize=False) self._save_btn = Button(description="Save", layout=Layout(width='auto')) @@ -90,6 +107,8 @@ def __init__(self, im_width=300, im_height=300, self._controls_box = HBox([self._navi, self._save_btn], layout=Layout(display='flex', justify_content='center', flex_flow='wrap', align_items='center')) + self._controls_box.add_class("im2im-annotator-class") + display(HTML("")) self._grid_box = CaptureGrid(grid_item=ImageButton, image_width=label_width, image_height=label_height, n_rows=n_rows, n_cols=n_cols) @@ -98,6 +117,7 @@ def __init__(self, im_width=300, im_height=300, self._labels_box = VBox(children = [self._grid_label, self._grid_box], layout=Layout(display='flex', justify_content='center', flex_wrap='wrap', align_items='center')) + self._navi._size_dropdown.observe(self.change_scale, names='value') super().__init__(header=None, left_sidebar=VBox([self._image, self._controls_box], layout=Layout(display='flex', justify_content='center', flex_wrap='wrap', align_items='center')), @@ -107,6 +127,12 @@ def __init__(self, im_width=300, im_height=300, pane_widths=(6, 4, 0), pane_heights=(1, 1, 1)) + + def change_scale(self, change): + new_scale = int(change['new']) + if new_scale: + self._image._image_rescale = new_scale / 100 + def on_client_ready(self, callback): self._image.observe_client_ready(callback) diff --git a/ipyannotator/navi_widget.py b/ipyannotator/navi_widget.py index 168bdb5..4ae61cf 100644 --- a/ipyannotator/navi_widget.py +++ b/ipyannotator/navi_widget.py @@ -5,17 +5,20 @@ # Internal Cell from ipywidgets import (AppLayout, Button, IntSlider, HBox, Output, - Layout, Label) -from traitlets import Int, observe, link, HasTraits + Layout, Label, Dropdown) +from IPython.display import display, HTML +from traitlets import Int, Bool, observe, link, HasTraits # Internal Cell class NaviGUI(HBox): max_im_number = Int(0) + disable_resize = Bool(True) def __init__(self): self._im_number_slider = IntSlider(min=0, max=self.max_im_number-1, - value=0, description='Image Nr.') + value=0, description='Image Nr.', style={'description_width': 'initial'}, + layout=Layout(width='250px')) self._prev_btn = Button(description='< Previous', layout=Layout(width='auto')) @@ -23,8 +26,23 @@ def __init__(self): self._next_btn = Button(description='Next >', layout=Layout(width='auto')) - super().__init__(children=[self._prev_btn, self._im_number_slider, self._next_btn], - layout=Layout(display='flex', flex_flow='row wrap', align_items='center')) + self._size_dropdown = Dropdown(options=['50', '75', '100', '150', '200'], + value='100', description="Size", disabled=self.disable_resize, + layout=Layout(width='auto'), style={'description_width': 'initial'}) + + self._im_number_slider.add_class("navi-class") + self._prev_btn.add_class("navi-class") + self._next_btn.add_class("navi-class") + self._size_dropdown.add_class("navi-class") + + display(HTML("")) + display(HTML("")) + + super().__init__(children=[self._prev_btn, self._im_number_slider, self._size_dropdown, + self._next_btn], layout=Layout(display='inline-flex', + flex_flow='row wrap', + align_items='center', + justify_content='space-around')) @observe('max_im_number') def check_im_num(self, change): @@ -37,6 +55,7 @@ def check_im_num(self, change): class NaviLogic(HasTraits): index = Int(0) max_im_number = Int(0) + disable_resize = Bool(True) def __init__(self): super().__init__() @@ -52,8 +71,9 @@ class Navi(NaviGUI): Represents simple navigation module with slider. """ - def __init__(self, max_im_number=1): + def __init__(self, max_im_number=1, disable_resize=True): self.max_im_number = max_im_number + self.disable_resize = disable_resize super().__init__() @@ -64,4 +84,5 @@ def __init__(self, max_im_number=1): # link slider value to button increment logic link((self._im_number_slider, 'value'), (self.model, 'index')) - link((self, 'max_im_number'), (self.model, 'max_im_number')) \ No newline at end of file + link((self, 'max_im_number'), (self.model, 'max_im_number')) + link((self, 'disable_resize'), (self.model, 'disable_resize')) \ No newline at end of file diff --git a/nbs/01_bbox_canvas.ipynb b/nbs/01_bbox_canvas.ipynb index 96d2a45..0ebfe22 100644 --- a/nbs/01_bbox_canvas.ipynb +++ b/nbs/01_bbox_canvas.ipynb @@ -285,15 +285,20 @@ "source": [ "#exporti\n", "\n", - "def draw_img(canvas, file, clear=False):\n", - " # draws resized image on canvas and returns scale used\n", + "def draw_img(canvas, file, clear=False, canvas_size=None, rescale=1.0):\n", + " # draws an image on canvas, a specific size can be passed to scale image\n", " with hold_canvas(canvas):\n", " if clear:\n", " canvas.clear()\n", "\n", " sprite1 = Image.from_file(file)\n", "\n", - " width_canvas, height_canvas = canvas.width, canvas.height\n", + " if canvas_size:\n", + " width_canvas, height_canvas = canvas_size\n", + " # if no specific size is passed, use current canvas size\n", + " else:\n", + " width_canvas, height_canvas = canvas.width, canvas.height\n", + "\n", " width_img, height_img = get_image_size(file)\n", "\n", " ratio_canvas = float(width_canvas) / height_canvas\n", @@ -307,8 +312,8 @@ " scale = height_canvas / height_img\n", "\n", " canvas.draw_image(sprite1, 0, 0, \n", - " width=width_img * min(1, scale),\n", - " height=height_img * min(1, scale))\n", + " width=(width_img * min(1, scale)) * rescale,\n", + " height=(height_img * min(1, scale)) * rescale)\n", " return scale" ] }, @@ -319,7 +324,7 @@ "outputs": [], "source": [ "file = \"../data/projects/bbox/pics/red400x640.png\"\n", - "canvas = Canvas(width=300, height=300)\n", + "canvas = Canvas(width=300, height=320)\n", "draw_bg(canvas)\n", "scale = draw_img(canvas, file)\n", "print(scale)\n", @@ -559,7 +564,7 @@ " else: # otherwise, save bbox values to backend\n", " self.bbox_coords = dict({ k: v / self._image_scale for k, v in self._canvas_bbox_coords.items() })\n", "# print(\"<- STOP DRAWING\")\n", - " \n", + "\n", " \n", " @traitlets.observe('bbox_coords')\n", " def _update_canvas_bbox_coords(self, change):\n", @@ -592,13 +597,12 @@ " \n", " def _clear_bbox(self):\n", " self._multi_canvas[self._box_layer].clear()\n", - " \n", - " \n", + "\n", " @traitlets.observe('image_path')\n", " def _draw_image(self, image):\n", " self._image_scale = draw_img(self._multi_canvas[self._image_layer], self.image_path, clear=True)\n", " self._im_name_box.value = Path(self.image_path).name\n", - "\n", + " \n", " @property\n", " def image_scale(self):\n", " return self._image_scale\n", @@ -612,13 +616,20 @@ " self._multi_canvas.on_client_ready(cb)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "gui = BBoxCanvas(width=100, height=100)\n", + "gui = BBoxCanvas(width=200, height=200)\n", "gui" ] }, diff --git a/nbs/01b_tutorial_image_classification.ipynb b/nbs/01b_tutorial_image_classification.ipynb index 1fd3467..bb01fa1 100644 --- a/nbs/01b_tutorial_image_classification.ipynb +++ b/nbs/01b_tutorial_image_classification.ipynb @@ -254,7 +254,7 @@ " n_cols=n_cols,\n", " label_autosize=False\n", " )\n", - "display(im2im)" + "im2im" ] }, { diff --git a/nbs/02_navi_widget.ipynb b/nbs/02_navi_widget.ipynb index 829aea6..54a8e08 100644 --- a/nbs/02_navi_widget.ipynb +++ b/nbs/02_navi_widget.ipynb @@ -36,8 +36,9 @@ "#exporti\n", "from ipywidgets import (AppLayout, Button, IntSlider,\n", " HBox, Output,\n", - " Layout, Label)\n", - "from traitlets import Int, observe, link, HasTraits " + " Layout, Label, Dropdown)\n", + "from IPython.display import display, HTML\n", + "from traitlets import Int, Bool, observe, link, HasTraits " ] }, { @@ -50,10 +51,12 @@ "\n", "class NaviGUI(HBox):\n", " max_im_number = Int(0)\n", + " disable_resize = Bool(True)\n", " \n", " def __init__(self):\n", " self._im_number_slider = IntSlider(min=0, max=self.max_im_number-1,\n", - " value=0, description='Image Nr.')\n", + " value=0, description='Image Nr.', style={'description_width': 'initial'},\n", + " layout=Layout(width='250px'))\n", " \n", " self._prev_btn = Button(description='< Previous',\n", " layout=Layout(width='auto'))\n", @@ -61,9 +64,24 @@ " self._next_btn = Button(description='Next >',\n", " layout=Layout(width='auto'))\n", " \n", - " super().__init__(children=[self._prev_btn, self._im_number_slider, self._next_btn],\n", - " layout=Layout(display='flex', flex_flow='row wrap', align_items='center'))\n", + " self._size_dropdown = Dropdown(options=['50', '75', '100', '150', '200'],\n", + " value='100', description=\"Size\", disabled=self.disable_resize,\n", + " layout=Layout(width='auto'), style={'description_width': 'initial'})\n", + "\n", + " self._im_number_slider.add_class(\"navi-class\")\n", + " self._prev_btn.add_class(\"navi-class\")\n", + " self._next_btn.add_class(\"navi-class\")\n", + " self._size_dropdown.add_class(\"navi-class\")\n", + " \n", + " display(HTML(\"\"))\n", + " display(HTML(\"\"))\n", " \n", + " super().__init__(children=[self._prev_btn, self._im_number_slider, self._size_dropdown,\n", + " self._next_btn], layout=Layout(display='inline-flex',\n", + " flex_flow='row wrap',\n", + " align_items='center',\n", + " justify_content='space-around'))\n", + "\n", " @observe('max_im_number')\n", " def check_im_num(self, change):\n", " if not hasattr(self, '_im_number_slider'):\n", @@ -82,6 +100,7 @@ "class NaviLogic(HasTraits):\n", " index = Int(0)\n", " max_im_number = Int(0)\n", + " disable_resize = Bool(True)\n", " \n", " def __init__(self):\n", " super().__init__()\n", @@ -104,8 +123,9 @@ " Represents simple navigation module with slider.\n", " \n", " \"\"\"\n", - " def __init__(self, max_im_number=1):\n", + " def __init__(self, max_im_number=1, disable_resize=True):\n", " self.max_im_number = max_im_number\n", + " self.disable_resize = disable_resize\n", " \n", " super().__init__()\n", " \n", @@ -116,7 +136,8 @@ " \n", " # link slider value to button increment logic\n", " link((self._im_number_slider, 'value'), (self.model, 'index'))\n", - " link((self, 'max_im_number'), (self.model, 'max_im_number'))" + " link((self, 'max_im_number'), (self.model, 'max_im_number'))\n", + " link((self, 'disable_resize'), (self.model, 'disable_resize'))" ] }, { @@ -149,9 +170,9 @@ ], "metadata": { "kernelspec": { - "display_name": "ipyannotator_env_local", + "display_name": "Python 3", "language": "python", - "name": "ipyannotator_env_local" + "name": "python3" } }, "nbformat": 4, diff --git a/nbs/06_capture_annotator.ipynb b/nbs/06_capture_annotator.ipynb index 1e8baed..10abf0a 100644 --- a/nbs/06_capture_annotator.ipynb +++ b/nbs/06_capture_annotator.ipynb @@ -251,6 +251,9 @@ " indent=False,\n", " layout=Layout(width='100px'))\n", "\n", + " self._none_checkbox.add_class(\"none-checkbox-class\")\n", + " display(HTML(\"\"))\n", + "\n", " self._controls_box = HBox([self._navi, self._save_btn, self._none_checkbox],\n", " layout=Layout(display='flex', justify_content='center', flex_flow='wrap', align_items='center'))\n", " \n", diff --git a/nbs/07_im2im_annotator.ipynb b/nbs/07_im2im_annotator.ipynb index a6d882f..68b1ca5 100644 --- a/nbs/07_im2im_annotator.ipynb +++ b/nbs/07_im2im_annotator.ipynb @@ -70,25 +70,42 @@ "class ImCanvas(HBox, HasTraits):\n", " image_path = Unicode()\n", " _image_scale = Float()\n", - " \n", + " _image_rescale = Float(1.0)\n", + "\n", " def __init__(self, width=150, height=150):\n", - " \n", + " \n", " self._canvas = Canvas(width=width, height=height)\n", + " self._initial_canvas_size = self._canvas.size\n", + "\n", + " super().__init__([self._canvas])\n", "\n", - " super().__init__([self._canvas]) \n", + " def _draw_image(self, canvas_size=None):\n", + " self._image_scale = draw_img(self._canvas, self.image_path, clear=True,\n", + " canvas_size=canvas_size, rescale=self._image_rescale)\n", "\n", " @observe('image_path')\n", - " def _draw_image(self, change):\n", - " self._image_scale = draw_img(self._canvas, self.image_path, clear=True)\n", + " def _call_draw_image(self, change):\n", + " self._draw_image(self._initial_canvas_size)\n", "\n", " # Add value as a read-only property\n", " @property\n", " def image_scale(self):\n", " return self._image_scale\n", - " \n", + "\n", + " @observe('_image_rescale')\n", + " def _redraw_image(self, change):\n", + " # Resize canvas\n", + " new_width = self._initial_canvas_size[0] * self._image_rescale\n", + " new_height = self._initial_canvas_size[1] * self._image_rescale\n", + " self._canvas.size = (new_width, new_height)\n", + " \n", + " # As draw_image method uses canvas current size, we pass\n", + " # as parameter the initial size of canvas (before rescaling it too)\n", + " self._draw_image(self._initial_canvas_size)\n", + "\n", " def _clear_image(self):\n", " self._canvas.clear()\n", - " \n", + "\n", " # needed to support voila\n", " # https://ipycanvas.readthedocs.io/en/latest/advanced.html#ipycanvas-in-voila\n", " def observe_client_ready(self, cb=None):\n", @@ -107,6 +124,15 @@ "im.image_scale" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "im._image_rescale = 2" + ] + }, { "cell_type": "code", "execution_count": null, @@ -148,7 +174,7 @@ " \n", " self._image = ImCanvas(width=im_width, height=im_height)\n", " \n", - " self._navi = Navi()\n", + " self._navi = Navi(disable_resize=False)\n", " \n", " self._save_btn = Button(description=\"Save\",\n", " layout=Layout(width='auto'))\n", @@ -157,14 +183,17 @@ " self._controls_box = HBox([self._navi, self._save_btn],\n", " layout=Layout(display='flex', justify_content='center', flex_flow='wrap', align_items='center'))\n", " \n", + " self._controls_box.add_class(\"im2im-annotator-class\")\n", + " display(HTML(\"\"))\n", " \n", " self._grid_box = CaptureGrid(grid_item=ImageButton, image_width=label_width, image_height=label_height, n_rows=n_rows, n_cols=n_cols)\n", "\n", "\n", " self._grid_label = HTML(value=\"LABEL\",)\n", " self._labels_box = VBox(children = [self._grid_label, self._grid_box],\n", - " layout=Layout(display='flex', justify_content='center', flex_wrap='wrap', align_items='center'))\n", - "\n", + " layout=Layout(display='flex', justify_content='center', flex_wrap='wrap', align_items='center')) \n", + " \n", + " self._navi._size_dropdown.observe(self.change_scale, names='value')\n", " \n", " super().__init__(header=None,\n", " left_sidebar=VBox([self._image, self._controls_box], layout=Layout(display='flex', justify_content='center', flex_wrap='wrap', align_items='center')),\n", @@ -173,6 +202,12 @@ " footer=None,\n", " pane_widths=(6, 4, 0),\n", " pane_heights=(1, 1, 1))\n", + " \n", + " \n", + " def change_scale(self, change):\n", + " new_scale = int(change['new'])\n", + " if new_scale:\n", + " self._image._image_rescale = new_scale / 100\n", " \n", " def on_client_ready(self, callback):\n", " self._image.observe_client_ready(callback)\n", @@ -201,11 +236,20 @@ "metadata": {}, "outputs": [], "source": [ - "im2im_ = Im2ImAnnotatorGUI(im_height = 500, im_width = 500, label_width=50, label_height=50, n_rows=2, n_cols=3)\n", + "im2im_ = Im2ImAnnotatorGUI(im_height = 100, im_width = 100, label_width=50, label_height=50, n_rows=2, n_cols=3)\n", "im2im_._image.image_path='../data/projects/im2im1/pics/Grass1.png'\n", "im2im_" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "im2im_._image._image_rescale = 2" + ] + }, { "cell_type": "code", "execution_count": null,