From 45efaad26361d7f9243dd7e226f63af02beafff9 Mon Sep 17 00:00:00 2001 From: Justin Brooks Date: Sun, 27 Jan 2019 12:41:57 -0500 Subject: [PATCH] HTML Formatting (#77) * Add imantics thumbnail * Html formatting * Show category with image thumbnail * Fixed create annotation bug * Removed unused buttons --- app/api/annotations.py | 4 +- app/api/categories.py | 2 +- app/api/datasets.py | 6 +- app/api/images.py | 6 +- app/models.py | 47 +++++- app/util/thumbnail_util.py | 46 ------ client/src/App.vue | 2 +- client/src/components/Metadata.vue | 27 +++- client/src/components/NavBar.vue | 22 ++- client/src/components/Pagination.vue | 15 +- client/src/components/Status.vue | 13 +- client/src/components/TagsInput.vue | 67 ++++++--- client/src/components/User.vue | 26 +++- .../src/components/annotator/Annotation.vue | 86 ++++++++--- client/src/components/annotator/Category.vue | 115 +++++++++++---- .../components/annotator/CustomShortcut.vue | 11 +- client/src/components/annotator/FileTitle.vue | 18 ++- client/src/components/annotator/Label.vue | 7 +- .../annotator/panels/BrushPanel.vue | 14 +- .../annotator/panels/EraserPanel.vue | 14 +- .../annotator/panels/MagicWandPanel.vue | 17 ++- .../annotator/panels/PanelButton.vue | 5 +- .../annotator/panels/PanelInputNumber.vue | 9 +- .../annotator/panels/PanelInputString.vue | 2 +- .../annotator/panels/PanelToggle.vue | 6 +- .../annotator/panels/PolygonPanel.vue | 28 +++- .../annotator/panels/SelectPanel.vue | 1 - .../annotator/tools/CopyAnnotationsButton.vue | 48 ++++-- .../annotator/tools/SettingsButton.vue | 50 +++++-- .../components/annotator/tools/UndoButton.vue | 1 - client/src/components/cards/CategoryCard.vue | 42 ++++-- client/src/components/cards/DatasetCard.vue | 130 +++++++++++++---- client/src/components/cards/ImageCard.vue | 54 +++++-- client/src/views/AdminPanel.vue | 101 ++++++++++--- client/src/views/Annotator.vue | 131 ++++++++++++----- client/src/views/Auth.vue | 137 ++++++++++++------ client/src/views/Categories.vue | 93 +++++++++--- client/src/views/Dataset.vue | 122 +++++++++++++--- client/src/views/Datasets.vue | 130 +++++++++++++---- client/src/views/Home.vue | 2 +- client/src/views/PageNotFound.vue | 10 +- client/src/views/Undo.vue | 50 +++++-- client/src/views/User.vue | 41 ++++-- requirements.txt | 3 +- 44 files changed, 1328 insertions(+), 433 deletions(-) delete mode 100644 app/util/thumbnail_util.py diff --git a/app/api/annotations.py b/app/api/annotations.py index 8b7cde36..d532794b 100644 --- a/app/api/annotations.py +++ b/app/api/annotations.py @@ -1,8 +1,9 @@ from flask_restplus import Namespace, Resource, reqparse from flask_login import login_required, current_user +from imantics import Color from ..models import AnnotationModel -from ..util import query_util, color_util +from ..util import query_util import datetime @@ -35,7 +36,6 @@ def post(self): try: annotation = AnnotationModel(image_id=image_id, category_id=category_id, metadata=metadata) - annotation.color = color_util.random_color_hex() if color is None else color annotation.save() except (ValueError, TypeError) as e: return {'message': str(e)}, 400 diff --git a/app/api/categories.py b/app/api/categories.py index 56c54142..07b0dd05 100644 --- a/app/api/categories.py +++ b/app/api/categories.py @@ -2,7 +2,7 @@ from flask_login import login_required, current_user from ..util.pagination_util import Pagination -from ..util import query_util, color_util +from ..util import query_util from ..models import CategoryModel, AnnotationModel import datetime diff --git a/app/api/datasets.py b/app/api/datasets.py index fc65f833..78e23f3a 100644 --- a/app/api/datasets.py +++ b/app/api/datasets.py @@ -241,7 +241,11 @@ def get(self, dataset_id): for image in images: image_id = image.get('id') - image['annotations'] = AnnotationModel.objects(image_id=image_id, deleted=False).count() + query = AnnotationModel.objects(image_id=image_id, deleted=False) + image['annotations'] = query.count() + category_ids = query.distinct('category_id') + image['categories'] = query_util.fix_ids(CategoryModel.objects(id__in=category_ids).only('name', 'color')) + subdirectories = [f for f in sorted(os.listdir(directory)) if os.path.isdir(directory + f)] diff --git a/app/api/images.py b/app/api/images.py index 961d3525..9dbf0dd8 100644 --- a/app/api/images.py +++ b/app/api/images.py @@ -3,7 +3,7 @@ from werkzeug.datastructures import FileStorage from flask import send_file -from ..util import query_util, coco_util, thumbnail_util +from ..util import query_util, coco_util from ..models import * import datetime @@ -184,7 +184,7 @@ def post(self, from_id, to_id): @api.route('//thumbnail') -class ImageCoco(Resource): +class ImageThumbnail(Resource): @api.expect(image_download) @login_required @@ -206,7 +206,7 @@ def get(self, image_id): if height < 1: height = image.height - pil_image = thumbnail_util.generate_thumbnail(image, save=False) + pil_image = image.thumbnail() pil_image.thumbnail((width, height), Image.ANTIALIAS) image_io = io.BytesIO() diff --git a/app/models.py b/app/models.py index 840182e8..563abf10 100644 --- a/app/models.py +++ b/app/models.py @@ -3,13 +3,14 @@ import json import datetime import numpy as np +import imantics as im +from PIL import Image from flask_mongoengine import MongoEngine from mongoengine.queryset.visitor import Q from flask_login import UserMixin, current_user -from .util import color_util from .config import Config from PIL import Image @@ -129,6 +130,10 @@ def thumbnail_path(self): os.makedirs(directory) return '/'.join(folders) + + def thumbnail(self): + image = self().draw(color_by_category=True, bbox=False) + return Image.fromarray(image) def copy_annotations(self, annotations): """ @@ -148,6 +153,15 @@ def copy_annotations(self, annotations): return annotations.count() + def __call__(self): + + image = im.Image.from_path(self.path) + for annotation in AnnotationModel.objects(image_id=self.id, deleted=False).all(): + if not annotation.is_empty(): + image.add(annotation()) + + return image + class AnnotationModel(db.DynamicDocument): @@ -196,7 +210,7 @@ def save(self, copy=False, *args, **kwargs): self.metadata = dataset.default_annotation_metadata.copy() if self.color is None: - self.color = color_util.random_color_hex() + self.color = im.Color.random().hex if current_user: self.creator = current_user.username @@ -225,6 +239,23 @@ def clone(self): return AnnotationModel(**create) + def __call__(self): + + category = CategoryModel.objects(id=self.category_id).first() + if category: + category = category() + + data = { + 'image': None, + 'category': category, + 'color': self.color, + 'polygons': self.segmentation, + 'width': self.width, + 'height': self.height, + 'metadata': self.metadata + } + + return im.Annotation(**data) class CategoryModel(db.DynamicDocument): @@ -260,7 +291,7 @@ def bulk_create(cls, categories): def save(self, *args, **kwargs): if not self.color: - self.color = color_util.random_color_hex() + self.color = im.Color.random().hex if current_user: self.creator = current_user.username @@ -269,6 +300,16 @@ def save(self, *args, **kwargs): return super(CategoryModel, self).save(*args, **kwargs) + def __call__(self): + """ Generates imantics category object """ + data = { + 'name': self.name, + 'color': self.color, + 'parent': self.supercategory, + 'metadata': self.metadata, + 'id': self.id + } + return im.Category(**data) class LicenseModel(db.DynamicDocument): id = db.SequenceField(primary_key=True) diff --git a/app/util/thumbnail_util.py b/app/util/thumbnail_util.py deleted file mode 100644 index 60dc926b..00000000 --- a/app/util/thumbnail_util.py +++ /dev/null @@ -1,46 +0,0 @@ -import numpy as np - -from ..models import AnnotationModel -from .color_util import hex_to_rgb - -from PIL import Image - - -def apply_mask(image, mask, color, alpha=0.5): - """Apply the given mask to the image. - """ - for c in range(3): - image[:, :, c] = np.where(mask == 1, - image[:, :, c] * - (1 - alpha) + alpha * color[c] * 255, - image[:, :, c]) - return image - - -def generate_thumbnail(image_model, save=True): - - image = Image.open(image_model.path) - binary_image = np.array(image) - binary_image.setflags(write=True) - - annotations = AnnotationModel.objects(image_id=image_model.id, deleted=False).all() - - for annotation in annotations: - - if len(annotation.segmentation) == 0: - continue - - color = np.array(hex_to_rgb(annotation.color))/255 - binary_image = apply_mask(binary_image, annotation.mask(), color) - - image = Image.fromarray(binary_image) - - if save: - image.save(image_model.thumbnail_path()) - - return image - - - - - diff --git a/client/src/App.vue b/client/src/App.vue index 5673c4b1..601c9844 100755 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,6 +1,6 @@ diff --git a/client/src/components/Metadata.vue b/client/src/components/Metadata.vue index 73aaa7b2..eab9ec1c 100755 --- a/client/src/components/Metadata.vue +++ b/client/src/components/Metadata.vue @@ -1,6 +1,10 @@