From 67b855428ea3a71a6c26b27cb54e79c5b997dac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Dav=C3=B3?= Date: Tue, 26 Oct 2021 18:32:51 +0200 Subject: [PATCH] #9: Showing grid of 4 sample images instead of one --- .gitignore | 5 ++- ipyannotator/__init__.py | 2 +- ipyannotator/_nbdev.py | 1 + ipyannotator/capture_annotator.py | 5 +++ ipyannotator/im2im_annotator.py | 60 ++++++++++++++++++++++++++- ipyannotator/image_button.py | 6 ++- nbs/05_image_button.ipynb | 6 ++- nbs/06_capture_annotator.ipynb | 5 +++ nbs/07_im2im_annotator.ipynb | 68 +++++++++++++++++++++++++++++-- 9 files changed, 149 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 97b11ce..2bb0ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,7 @@ tools/fastai checklink/cookies.txt #ipyannotator -**/**/autogenerated*/ \ No newline at end of file +**/**/autogenerated*/ + +#nbs +nbs/data \ No newline at end of file diff --git a/ipyannotator/__init__.py b/ipyannotator/__init__.py index a8d4557..6a9beea 100644 --- a/ipyannotator/__init__.py +++ b/ipyannotator/__init__.py @@ -1 +1 @@ -__version__ = "0.3.5" +__version__ = "0.4.0" diff --git a/ipyannotator/_nbdev.py b/ipyannotator/_nbdev.py index 38d0661..d56323f 100644 --- a/ipyannotator/_nbdev.py +++ b/ipyannotator/_nbdev.py @@ -42,6 +42,7 @@ "ImCanvas": "07_im2im_annotator.ipynb", "Im2ImAnnotatorGUI": "07_im2im_annotator.ipynb", "text_on_img": "07_im2im_annotator.ipynb", + "grid_imgs": "07_im2im_annotator.ipynb", "flatten": "07_im2im_annotator.ipynb", "reconstruct_class_images": "07_im2im_annotator.ipynb", "Im2ImAnnotatorLogic": "07_im2im_annotator.ipynb", diff --git a/ipyannotator/capture_annotator.py b/ipyannotator/capture_annotator.py index 6483392..6057d5d 100644 --- a/ipyannotator/capture_annotator.py +++ b/ipyannotator/capture_annotator.py @@ -28,6 +28,7 @@ class CaptureGrid(GridBox, HasTraits): """ debug_output = Output(layout={'border': '1px solid black'}) current_state = Dict() + autogenerate_idx = Int() def __init__(self, grid_item=ImageButton, image_width=150, image_height=150, n_rows=3, n_cols=3, display_label=False): @@ -64,6 +65,10 @@ def __init__(self, grid_item=ImageButton, image_width=150, image_height=150, super().__init__(children=self._labels, layout=Layout(**centered_settings)) + @observe('autogenerate_idx') + def _autogenerate_idx_changed(self, change): + for i in self._labels: + i._read_image() @debug_output.capture(clear_output=True) def on_state_change(self, change=None): diff --git a/ipyannotator/im2im_annotator.py b/ipyannotator/im2im_annotator.py index f9dc067..d8756d4 100644 --- a/ipyannotator/im2im_annotator.py +++ b/ipyannotator/im2im_annotator.py @@ -12,12 +12,13 @@ import re from functools import partial +import itertools as it from math import ceil from pathlib import Path from ipycanvas import Canvas, hold_canvas from ipyevents import Event -from ipywidgets import (AppLayout, VBox, HBox, Button, GridBox, Layout, Checkbox, HTML, IntText, Valid, Output, Image) +from ipywidgets import (AppLayout, VBox, HBox, Button, GridBox, Layout, Checkbox, HTML, IntText, Valid, Output) from traitlets import Dict, Int, Float, HasTraits, observe, dlink, link, List, Unicode from .navi_widget import Navi @@ -173,6 +174,41 @@ def text_on_img(text="Hello", lbl_w=None, lbl_h=None, font_size=14, filepath=Non # Internal Cell +#exporti + +def grid_imgs(images, lbl_w=None, lbl_h=None, filepath=None,maxcols=2,maxrows=2): + assert(images) + + if lbl_w is None: + lbl_w = 150 + + if lbl_h is None: + lbl_h = 150 + + grid = Image.new(mode="RGB", size=(lbl_w, lbl_h)) + cols, rows = (maxcols, maxrows) if len(images) >= maxcols*maxrows else (1, 1) + + w, h = lbl_w // cols, lbl_h // rows + subset = images[:cols*rows] + + # We only use the first images of the subset + for i, imgpath in enumerate(subset): + # Using NEAREST as its the fastest and the images will be small + original = Image.open(imgpath) + ratio = original.width/original.height + neww, newh = int(min(w,h)*ratio), int(min(w,h)) + img = original.resize((neww, newh), Image.NEAREST) + x = i%cols*w + (w-neww)//2 + y = i//rows*h + (h-newh)//2 + grid.paste(img, (x, y)) + + if filepath: + grid.save(filepath) + + return grid + +# Internal Cell + try: from collections.abc import Iterable except ImportError: @@ -199,7 +235,17 @@ def reconstruct_class_images(label_dir, annotation_file, lbl_w=None, lbl_h=None) cl_im_name = f'{cl_name}.jpg' if not re.findall("([-\w]+\.(?:jpg|png|jpeg))", cl_name, re.IGNORECASE) else cl_name - text_on_img(text = os.path.splitext(cl_name)[0], filepath = label_dir/cl_im_name, lbl_w=lbl_w, lbl_h=lbl_h) + # Searching for a bunch of sample images + # We get a list with up to four images to avoid iterating through all the data + sample_images = list(it.islice((im_path for im_path,classes in data.items() + if classes and + cl_name in flatten(classes) and + Path(im_path).is_file()), 4)) + + if sample_images: + grid_imgs(sample_images, filepath = label_dir/cl_im_name,lbl_w=lbl_w, lbl_h=lbl_h,maxcols=2,maxrows=2) + else: + text_on_img(text = os.path.splitext(cl_name)[0], filepath = label_dir/cl_im_name, lbl_w=lbl_w, lbl_h=lbl_h) # Internal Cell @@ -211,6 +257,7 @@ class Im2ImAnnotatorLogic(HasTraits): disp_number = Int() # number of labels on screen label_state = Dict() question_value = Unicode() + autogenerate_idx = Int() def __init__(self, project_path, file_name=None, question=None, @@ -219,6 +266,8 @@ def __init__(self, project_path, file_name=None, question=None, self.project_path = Path(project_path) self.step_down = step_down + self.lbl_w = lbl_w + self.lbl_h = lbl_h self.image_dir, self.annotation_file_path = setup_project_paths(self.project_path, file_name=file_name, image_dir=image_dir, @@ -227,6 +276,7 @@ def __init__(self, project_path, file_name=None, question=None, # artificialy generate labels if no class images given if label_dir is None: self.label_dir = Path(self.project_path, 'class_autogenerated_' + ''.join(random.sample(str(uuid.uuid4()), 5))) + self.autogenerate_idx = 1 self.label_dir.mkdir(parents=True, exist_ok=True) question = 'Autogenerated classes' @@ -237,6 +287,7 @@ def __init__(self, project_path, file_name=None, question=None, text_on_img(text = 'None', filepath = self.label_dir /'None.jpg', lbl_w=lbl_w, lbl_h=lbl_h) else: self.label_dir = Path(self.project_path, label_dir) + self.autogenerate_idx = 0 # select images and labels only given annotatin file @@ -313,6 +364,10 @@ def _save_annotations(self, *args, **kwargs): # to disk self._update_annotations(index) self.annotations.save(self.annotation_file_path) + if self.autogenerate_idx: + reconstruct_class_images(self.label_dir, self.annotation_file_path, lbl_w=self.lbl_w, lbl_h=self.lbl_h) + self.autogenerate_idx += 1 + @observe('index') def _idx_changed(self, change): @@ -388,6 +443,7 @@ def __init__(self, project_path, file_name=None, image_dir=None, step_down=False # link state of model and grid box visualizer link((self._model, 'label_state'), (self._grid_box, 'current_state')) + link((self._model, 'autogenerate_idx'), (self._grid_box, 'autogenerate_idx')) def to_dict(self, only_annotated=True): diff --git a/ipyannotator/image_button.py b/ipyannotator/image_button.py index 2008f63..77139e6 100644 --- a/ipyannotator/image_button.py +++ b/ipyannotator/image_button.py @@ -63,7 +63,11 @@ def __init__(self, im_path=None, label=None, @observe('image_path') def _read_image(self, change=None): - new_path = change['new'] + if change: + new_path = change["new"] + else: + new_path = self.image_path + if new_path: self.image.value = open(new_path, "rb").read() if not self.children: diff --git a/nbs/05_image_button.ipynb b/nbs/05_image_button.ipynb index 344939a..74f71cf 100644 --- a/nbs/05_image_button.ipynb +++ b/nbs/05_image_button.ipynb @@ -102,7 +102,11 @@ " \n", " @observe('image_path')\n", " def _read_image(self, change=None):\n", - " new_path = change['new']\n", + " if change:\n", + " new_path = change[\"new\"]\n", + " else:\n", + " new_path = self.image_path\n", + "\n", " if new_path:\n", " self.image.value = open(new_path, \"rb\").read()\n", " if not self.children:\n", diff --git a/nbs/06_capture_annotator.ipynb b/nbs/06_capture_annotator.ipynb index 1e8baed..6a4f05f 100644 --- a/nbs/06_capture_annotator.ipynb +++ b/nbs/06_capture_annotator.ipynb @@ -67,6 +67,7 @@ " \"\"\"\n", " debug_output = Output(layout={'border': '1px solid black'})\n", " current_state = Dict()\n", + " autogenerate_idx = Int()\n", " \n", " def __init__(self, grid_item=ImageButton, image_width=150, image_height=150, \n", " n_rows=3, n_cols=3, display_label=False):\n", @@ -103,6 +104,10 @@ " \n", " super().__init__(children=self._labels, layout=Layout(**centered_settings))\n", " \n", + " @observe('autogenerate_idx')\n", + " def _autogenerate_idx_changed(self, change):\n", + " for i in self._labels:\n", + " i._read_image()\n", " \n", " @debug_output.capture(clear_output=True)\n", " def on_state_change(self, change=None):\n", diff --git a/nbs/07_im2im_annotator.ipynb b/nbs/07_im2im_annotator.ipynb index a6d882f..be3c5bb 100644 --- a/nbs/07_im2im_annotator.ipynb +++ b/nbs/07_im2im_annotator.ipynb @@ -44,12 +44,13 @@ "import re\n", "\n", "from functools import partial\n", + "import itertools as it\n", "from math import ceil\n", "from pathlib import Path\n", "\n", "from ipycanvas import Canvas, hold_canvas\n", "from ipyevents import Event\n", - "from ipywidgets import (AppLayout, VBox, HBox, Button, GridBox, Layout, Checkbox, HTML, IntText, Valid, Output, Image)\n", + "from ipywidgets import (AppLayout, VBox, HBox, Button, GridBox, Layout, Checkbox, HTML, IntText, Valid, Output)\n", "from traitlets import Dict, Int, Float, HasTraits, observe, dlink, link, List, Unicode\n", "\n", "from ipyannotator.navi_widget import Navi\n", @@ -402,6 +403,47 @@ "text_on_img(text=\"new labe\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "#exporti\n", + "\n", + "def grid_imgs(images, lbl_w=None, lbl_h=None, filepath=None,maxcols=2,maxrows=2):\n", + " assert(images)\n", + "\n", + " if lbl_w is None:\n", + " lbl_w = 150\n", + " \n", + " if lbl_h is None:\n", + " lbl_h = 150\n", + "\n", + " grid = Image.new(mode=\"RGB\", size=(lbl_w, lbl_h))\n", + " cols, rows = (maxcols, maxrows) if len(images) >= maxcols*maxrows else (1, 1)\n", + "\n", + " w, h = lbl_w // cols, lbl_h // rows\n", + " subset = images[:cols*rows]\n", + "\n", + " # We only use the first images of the subset\n", + " for i, imgpath in enumerate(subset):\n", + " # Using NEAREST as its the fastest and the images will be small\n", + " original = Image.open(imgpath)\n", + " ratio = original.width/original.height\n", + " neww, newh = int(min(w,h)*ratio), int(min(w,h))\n", + " img = original.resize((neww, newh), Image.NEAREST)\n", + " x = i%cols*w + (w-neww)//2\n", + " y = i//rows*h + (h-newh)//2\n", + " grid.paste(img, (x, y))\n", + "\n", + " if filepath:\n", + " grid.save(filepath)\n", + "\n", + " return grid" + ] + }, { "cell_type": "code", "execution_count": null, @@ -460,8 +502,18 @@ " cl_name = \"None\"\n", " \n", " cl_im_name = f'{cl_name}.jpg' if not re.findall(\"([-\\w]+\\.(?:jpg|png|jpeg))\", cl_name, re.IGNORECASE) else cl_name\n", - " \n", - " text_on_img(text = os.path.splitext(cl_name)[0], filepath = label_dir/cl_im_name, lbl_w=lbl_w, lbl_h=lbl_h)" + "\n", + " # Searching for a bunch of sample images\n", + " # We get a list with up to four images to avoid iterating through all the data\n", + " sample_images = list(it.islice((im_path for im_path,classes in data.items()\n", + " if classes and \n", + " cl_name in flatten(classes) and\n", + " Path(im_path).is_file()), 4))\n", + "\n", + " if sample_images:\n", + " grid_imgs(sample_images, filepath = label_dir/cl_im_name,lbl_w=lbl_w, lbl_h=lbl_h,maxcols=2,maxrows=2)\n", + " else:\n", + " text_on_img(text = os.path.splitext(cl_name)[0], filepath = label_dir/cl_im_name, lbl_w=lbl_w, lbl_h=lbl_h)" ] }, { @@ -502,6 +554,7 @@ " disp_number = Int() # number of labels on screen\n", " label_state = Dict()\n", " question_value = Unicode()\n", + " autogenerate_idx = Int()\n", "\n", " \n", " def __init__(self, project_path, file_name=None, question=None, \n", @@ -510,6 +563,8 @@ " \n", " self.project_path = Path(project_path)\n", " self.step_down = step_down\n", + " self.lbl_w = lbl_w\n", + " self.lbl_h = lbl_h\n", " self.image_dir, self.annotation_file_path = setup_project_paths(self.project_path,\n", " file_name=file_name,\n", " image_dir=image_dir,\n", @@ -518,6 +573,7 @@ " # artificialy generate labels if no class images given\n", " if label_dir is None:\n", " self.label_dir = Path(self.project_path, 'class_autogenerated_' + ''.join(random.sample(str(uuid.uuid4()), 5)))\n", + " self.autogenerate_idx = 1\n", " self.label_dir.mkdir(parents=True, exist_ok=True)\n", " \n", " question = 'Autogenerated classes'\n", @@ -528,6 +584,7 @@ " text_on_img(text = 'None', filepath = self.label_dir /'None.jpg', lbl_w=lbl_w, lbl_h=lbl_h) \n", " else:\n", " self.label_dir = Path(self.project_path, label_dir)\n", + " self.autogenerate_idx = 0\n", " \n", " \n", " # select images and labels only given annotatin file\n", @@ -603,6 +660,10 @@ " index = kwargs.pop('old_index', self.index)\n", " self._update_annotations(index) \n", " self.annotations.save(self.annotation_file_path)\n", + "\n", + " if self.autogenerate_idx:\n", + " reconstruct_class_images(self.label_dir, self.annotation_file_path, lbl_w=self.lbl_w, lbl_h=self.lbl_h)\n", + " self.autogenerate_idx += 1\n", " \n", " \n", " @observe('index')\n", @@ -715,6 +776,7 @@ " \n", " # link state of model and grid box visualizer\n", " link((self._model, 'label_state'), (self._grid_box, 'current_state'))\n", + " link((self._model, 'autogenerate_idx'), (self._grid_box, 'autogenerate_idx'))\n", " \n", " \n", " def to_dict(self, only_annotated=True):\n",