diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11041c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.egg-info diff --git a/.vscode/.ropeproject/config.py b/.vscode/.ropeproject/config.py new file mode 100644 index 0000000..dee2d1a --- /dev/null +++ b/.vscode/.ropeproject/config.py @@ -0,0 +1,114 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + # prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + # prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + # prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from import ` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will remove all top-level import statements and + # reinsert them at the top of the module when making changes. + prefs['pull_imports_to_top'] = True + + # If `True`, rope will sort imports alphabetically by module name instead + # of alphabetically by import statement, with from imports after normal + # imports. + prefs['sort_imports_alphabetically'] = False + + # Location of implementation of + # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general + # case, you don't have to change this value, unless you're an rope expert. + # Change this value to inject you own implementations of interfaces + # listed in module rope.base.oi.type_hinting.providers.interfaces + # For example, you can add you own providers for Django Models, or disable + # the search type-hinting in a class hierarchy, etc. + prefs['type_hinting_factory'] = ( + 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/.vscode/.ropeproject/objectdb b/.vscode/.ropeproject/objectdb new file mode 100644 index 0000000..0a47446 Binary files /dev/null and b/.vscode/.ropeproject/objectdb differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc0ea5e --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Manu NALEPA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ad47f31 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include bounding_box *.ttf diff --git a/bounding_box/Ubuntu-B.ttf b/bounding_box/Ubuntu-B.ttf new file mode 100644 index 0000000..c0142fe Binary files /dev/null and b/bounding_box/Ubuntu-B.ttf differ diff --git a/bounding_box/__init__.py b/bounding_box/__init__.py new file mode 100644 index 0000000..10939f0 --- /dev/null +++ b/bounding_box/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.2' diff --git a/bounding_box/__init__.pyc b/bounding_box/__init__.pyc new file mode 100644 index 0000000..75d0810 Binary files /dev/null and b/bounding_box/__init__.pyc differ diff --git a/bounding_box/bounding_box.py b/bounding_box/bounding_box.py new file mode 100644 index 0000000..999e100 --- /dev/null +++ b/bounding_box/bounding_box.py @@ -0,0 +1,131 @@ +from __future__ import division as _division +from __future__ import print_function as _print_function + +import os as _os +import os.path as _path +import numpy as np +import cv2 as _cv2 +from PIL import ImageFont +import numpy as _np +from hashlib import md5 as _md5 + +_LOC = _path.realpath(_path.join(_os.getcwd(),_path.dirname(__file__))) + +#https://clrs.cc/ +_COLOR_NAME_TO_RGB = dict( + navy=((0, 38, 63), (119, 193, 250)), + blue=((0, 120, 210), (173, 220, 252)), + aqua=((115, 221, 252), (0, 76, 100)), + teal=((15, 205, 202), (0, 0, 0)), + olive=((52, 153, 114), (25, 58, 45)), + green=((0, 204, 84), (15, 64, 31)), + lime=((1, 255, 127), (0, 102, 53)), + yellow=((255, 216, 70), (103, 87, 28)), + orange=((255, 125, 57), (104, 48, 19)), + red=((255, 47, 65), (131, 0, 17)), + maroon=((135, 13, 75), (239, 117, 173)), + fuchsia=((246, 0, 184), (103, 0, 78)), + purple=((179, 17, 193), (241, 167, 244)), + black=((24, 24, 24), (220, 220, 220)), + gray=((168, 168, 168), (0, 0, 0)), + silver=((220, 220, 220), (0, 0, 0)) +) + +_COLOR_NAMES = list(_COLOR_NAME_TO_RGB) + +_DEFAULT_COLOR_NAME = "green" + +_FONT_PATH = _os.path.join(_LOC, "Ubuntu-B.ttf") +_FONT_HEIGHT = 15 +_FONT = ImageFont.truetype(_FONT_PATH, _FONT_HEIGHT) + +def _rgb_to_bgr(color): + return list(reversed(color)) + +def _color_image(image, font_color, background_color): + return background_color + (font_color - background_color) * image / 255 + +def _get_label_image(text, font_color_tuple_bgr, background_color_tuple_bgr): + text_image = _FONT.getmask(text) + shape = list(reversed(text_image.size)) + bw_image = np.array(text_image).reshape(shape) + + image = [ + _color_image(bw_image, font_color, background_color)[None, ...] + for font_color, background_color + in zip(font_color_tuple_bgr, background_color_tuple_bgr) + ] + + return np.concatenate(image).transpose(1, 2, 0) + +def add_bounding_box(image, left, top, right, bottom, label=None, color=None): + if type(image) is not _np.ndarray: + raise TypeError("'image' parameter must be a numpy.ndarray") + try: + left, top, right, bottom = int(left), int(top), int(right), int(bottom) + except ValueError: + raise TypeError("'left', 'top', 'right' & 'bottom' must be a number") + + image_height, image_width, _ = image.shape + + if not 0 <= top <= image_height: + raise TypeError("'top' must be between 0 and " + str(image_height)) + + if not 0 <= bottom <= image_height: + raise TypeError("'bottom' must be between 0 and " + str(image_height)) + + if not 0 <= left <= image_width: + raise TypeError("'left' must be between 0 and " + str(image_width)) + + if not 0 <= right <= image_width: + raise TypeError("'right' must be between 0 and " + str(image_width)) + + if label and type(label) is not str: + raise TypeError("'label' must be a str") + + if label and not color: + hex_digest = _md5(label.encode()).hexdigest() + color_index = int(hex_digest, 16) % len(_COLOR_NAME_TO_RGB) + color = _COLOR_NAMES[color_index] + + if not color: + color = _DEFAULT_COLOR_NAME + + if type(color) is not str: + raise TypeError("'color' must be a str") + + if color not in _COLOR_NAME_TO_RGB: + msg = "'color' must be one of " + ", ".join(_COLOR_NAME_TO_RGB) + raise ValueError(msg) + + colors = [_rgb_to_bgr(item) for item in _COLOR_NAME_TO_RGB[color]] + color, color_text = colors + + _cv2.rectangle(image, (left, top), (right, bottom), color, 2) + + if label: + label_image = _get_label_image(label, color_text, color) + label_height, label_width, _ = label_image.shape + + rectangle_bottom = top + rectangle_left = left - 1 + + rectangle_top = rectangle_bottom - label_height - 1 + rectangle_right = rectangle_left + 1 + label_width + + label_top = rectangle_top + 1 + + if rectangle_top < 0: + rectangle_top = top + rectangle_bottom = rectangle_top + label_height + 1 + + label_top = rectangle_top + + label_left = rectangle_left + 1 + label_bottom = label_top + label_height + label_right = label_left + label_width + + _cv2.rectangle(image, (rectangle_left, rectangle_top), + (rectangle_right, rectangle_bottom), color, -1) + + image[label_top:label_bottom, label_left:label_right, :] = label_image diff --git a/bounding_box/bounding_box.pyc b/bounding_box/bounding_box.pyc new file mode 100644 index 0000000..592fe0c Binary files /dev/null and b/bounding_box/bounding_box.pyc differ diff --git a/docs/examples.py b/docs/examples.py new file mode 100755 index 0000000..f0e582c --- /dev/null +++ b/docs/examples.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +from __future__ import print_function +import cv2 +from bounding_box import bounding_box as bb +import os + +def show_and_save(title, image, path): + cv2.imwrite(path, image) + cv2.imshow(title, image) + print("Press 'Enter' to display the next picture...") + cv2.waitKey(0) + cv2.destroyAllWindows() + +def main(): + in_path = os.path.join("docs", "images", "winton.jpg") + out_path = os.path.join("docs", "images", "winton_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 281, 12, 744, 431, "Winton", "maroon") + bb.add_bounding_box(image, 166, 149, 500, 297, "Trumpet", "yellow") + show_and_save("Winton MARSALIS", image, out_path) + + in_path = os.path.join("docs", "images", "khatia.jpg") + out_path = os.path.join("docs", "images", "khatia_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 280, 24, 802, 593, "Khatia", "maroon") + bb.add_bounding_box(image, 687, 1, 1448, 648, "Piano", "gray") + bb.add_bounding_box(image, 888, 492, 1190, 536, "Text") + show_and_save("Khatia BUNIATISHVILI", image, out_path) + + in_path = os.path.join("docs", "images", "clarifloue.jpg") + out_path = os.path.join("docs", "images", "clarifloue_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 69, 86, 470, 136, label="Headache designer") + bb.add_bounding_box(image, 136, 196, 406, 234, "Text") + bb.add_bounding_box(image, 67, 351, 471, 400, "Headache designer") + bb.add_bounding_box(image, 130, 456, 390, 494, "Text") + show_and_save("Clarinet", image, out_path) + + in_path = os.path.join("docs", "images", "nao-romeo-pepper.jpg") + out_path = os.path.join("docs", "images", "nao-romeo-pepper_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 155, 152, 244, 297, "Nao") + bb.add_bounding_box(image, 260, 6, 423, 416, "Romeo") + bb.add_bounding_box(image, 421, 76, 547, 402, "Pepper") + show_and_save("Robots", image, out_path) + + in_path = os.path.join("docs", "images", "ski-paraglider.jpg") + out_path = os.path.join("docs", "images", "ski-paraglider_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 0, 128, 645, 589, "Paraglider", "orange") + bb.add_bounding_box(image, 689, 442, 818, 566, "Skier", "gray") + show_and_save("Ski and paraglider", image, out_path) + + in_path = os.path.join("docs", "images", "paragliders.jpg") + out_path = os.path.join("docs", "images", "paragliders_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 90, 228, 318, 428, "Paraglider") + bb.add_bounding_box(image, 521, 110, 656, 415, "Paraglider") + show_and_save("Pretty Bounding Box", image, out_path) + + in_path = os.path.join("docs", "images", "selfie.jpg") + out_path = os.path.join("docs", "images", "selfie_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 5, 7, 150, 169, "Female", "fuchsia") + bb.add_bounding_box(image, 116, 7, 193, 113, "Male", "blue") + bb.add_bounding_box(image, 189, 7, 291, 124, "Female", "fuchsia") + bb.add_bounding_box(image, 288, 25, 355, 114, "Male", "blue") + bb.add_bounding_box(image, 367, 0, 448, 92, "Male", "blue") + bb.add_bounding_box(image, 435, 29, 506, 104, "Female", "fuchsia") + bb.add_bounding_box(image, 497, 3, 597, 111, "Female", "fuchsia") + bb.add_bounding_box(image, 110, 133, 213, 245, "Female", "fuchsia") + bb.add_bounding_box(image, 176, 120, 293, 289, "Female", "fuchsia") + bb.add_bounding_box(image, 314, 115, 470, 357, "Male", "blue") + bb.add_bounding_box(image, 468, 72, 577, 226, "Male", "blue") + show_and_save("The Selfie", image, out_path) + + in_path = os.path.join("docs", "images", "pobb.jpg") + out_path = os.path.join("docs", "images", "pobb_bb.png") + image = cv2.imread(in_path, cv2.IMREAD_COLOR) + bb.add_bounding_box(image, 76, 62, 155, 271, "Female", "fuchsia") + bb.add_bounding_box(image, 157, 44, 288, 274, "Male", "blue") + bb.add_bounding_box(image, 224, 64, 317, 274, "Male", "blue") + bb.add_bounding_box(image, 290, 48, 383, 277, "Male", "blue") + bb.add_bounding_box(image, 350, 42, 458, 276, "Female", "fuchsia") + bb.add_bounding_box(image, 416, 17, 510, 279, "Male", "blue") + bb.add_bounding_box(image, 482, 55, 573, 278, "Female", "fuchsia") + bb.add_bounding_box(image, 547, 63, 615, 277, "Female", "fuchsia") + bb.add_bounding_box(image, 608, 49, 704, 275, "Female", "fuchsia") + bb.add_bounding_box(image, 672, 34, 767, 274, "Male", "blue") + bb.add_bounding_box(image, 725, 62, 813, 273, "Female", "fuchsia") + bb.add_bounding_box(image, 786, 38, 887, 267, "Male", "blue") + bb.add_bounding_box(image, 864, 51, 959, 266, "Male", "blue") + show_and_save("POBB", image, out_path) + +if __name__ == "__main__": + main() diff --git a/docs/images/clarifloue.jpg b/docs/images/clarifloue.jpg new file mode 100644 index 0000000..313b523 Binary files /dev/null and b/docs/images/clarifloue.jpg differ diff --git a/docs/images/clarifloue_bb.png b/docs/images/clarifloue_bb.png new file mode 100644 index 0000000..386ca8c Binary files /dev/null and b/docs/images/clarifloue_bb.png differ diff --git a/docs/images/khatia.jpg b/docs/images/khatia.jpg new file mode 100644 index 0000000..7ece920 Binary files /dev/null and b/docs/images/khatia.jpg differ diff --git a/docs/images/khatia_bb.png b/docs/images/khatia_bb.png new file mode 100644 index 0000000..42e0123 Binary files /dev/null and b/docs/images/khatia_bb.png differ diff --git a/docs/images/nao-romeo-pepper.jpg b/docs/images/nao-romeo-pepper.jpg new file mode 100644 index 0000000..aafef83 Binary files /dev/null and b/docs/images/nao-romeo-pepper.jpg differ diff --git a/docs/images/nao-romeo-pepper_bb.png b/docs/images/nao-romeo-pepper_bb.png new file mode 100644 index 0000000..fe549da Binary files /dev/null and b/docs/images/nao-romeo-pepper_bb.png differ diff --git a/docs/images/paragliders.jpg b/docs/images/paragliders.jpg new file mode 100644 index 0000000..a7bb9fd Binary files /dev/null and b/docs/images/paragliders.jpg differ diff --git a/docs/images/paragliders_bb.png b/docs/images/paragliders_bb.png new file mode 100644 index 0000000..c1dbcdf Binary files /dev/null and b/docs/images/paragliders_bb.png differ diff --git a/docs/images/pobb.jpg b/docs/images/pobb.jpg new file mode 100644 index 0000000..e490410 Binary files /dev/null and b/docs/images/pobb.jpg differ diff --git a/docs/images/pobb_bb.png b/docs/images/pobb_bb.png new file mode 100644 index 0000000..aa4f343 Binary files /dev/null and b/docs/images/pobb_bb.png differ diff --git a/docs/images/selfie.jpg b/docs/images/selfie.jpg new file mode 100644 index 0000000..c8d9076 Binary files /dev/null and b/docs/images/selfie.jpg differ diff --git a/docs/images/selfie_bb.png b/docs/images/selfie_bb.png new file mode 100644 index 0000000..29b9ad9 Binary files /dev/null and b/docs/images/selfie_bb.png differ diff --git a/docs/images/ski-paraglider.jpg b/docs/images/ski-paraglider.jpg new file mode 100644 index 0000000..d69c59b Binary files /dev/null and b/docs/images/ski-paraglider.jpg differ diff --git a/docs/images/ski-paraglider_bb.png b/docs/images/ski-paraglider_bb.png new file mode 100644 index 0000000..66555d3 Binary files /dev/null and b/docs/images/ski-paraglider_bb.png differ diff --git a/docs/images/winton.jpg b/docs/images/winton.jpg new file mode 100644 index 0000000..ca05405 Binary files /dev/null and b/docs/images/winton.jpg differ diff --git a/docs/images/winton_bb.png b/docs/images/winton_bb.png new file mode 100644 index 0000000..4c74a61 Binary files /dev/null and b/docs/images/winton_bb.png differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8ba5679 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +from setuptools import setup, find_packages + +install_requires = [ + 'opencv-python >= 4.1.0', + 'Pillow >= 6.0.0', +] + +setup( + name='bounding_box', + version='0.1.2', + + python_requires='>=2.7', + + packages=find_packages(), + include_package_data=True, + + author='Manu NALEPA', + author_email='nalepae@gmail.com', + description='A tool to plot pretty bounding boxes around objects.', + long_description='See https://github.com/nalepae/bounding_box/tree/v1.0.0 for complete user guide.', + url='https://github.com/nalepae/bounding_box', + install_requires=install_requires, + license='BSD', +)