From d4f7758a984512683294a3f5f8f574e24079ecde Mon Sep 17 00:00:00 2001 From: thatgeeman Date: Thu, 20 Jan 2022 21:21:03 +0100 Subject: [PATCH] Patch 5: Minor fixes (#5) - Fixed `load_ann=True` - return default `anns` in `get_example` as json - added `stack_bxs` method --- .idea/misc.xml | 4 --- Pipfile | 2 +- Pipfile.lock | 10 +++--- ...ugh.ipynb => pybx_walkthrough_0.1.3.ipynb} | 0 src/pybx/basics.py | 36 +++++++++++++++---- src/pybx/sample.py | 12 ++++--- src/pybx/version.py | 3 +- src/pybx/vis.py | 6 ++-- tests/test_basics.py | 11 +++++- tests/test_vis.py | 29 ++++++++------- 10 files changed, 74 insertions(+), 39 deletions(-) delete mode 100644 .idea/misc.xml rename nbs/{pybx_walkthrough.ipynb => pybx_walkthrough_0.1.3.ipynb} (100%) diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index bcea7de..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Pipfile b/Pipfile index 2287885..66ca4c6 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] fastcore = ">=1.3.20" numpy = "==1.21" -pillow = "==9.0" +pillow = ">=7.1" matplotlib = "==3.5" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index eee07d7..77598c4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c101092adf3fd60eed1cecb28a11749972d93a2644128ec496180d009517c368" + "sha256": "b87116a03a8e637582f8a1baa79e9504cbe5dc5ef1dae5c34fe4f810a3b628eb" }, "pipfile-spec": 6, "requires": { @@ -209,7 +209,7 @@ "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4" ], "index": "pypi", - "version": "==9.0" + "version": "==9.0.0" }, "pip": { "hashes": [ @@ -245,11 +245,11 @@ }, "setuptools-scm": { "hashes": [ - "sha256:54f5b1dcccd28e7813236609e620671f23d548548d744270a07d8df6df623649", - "sha256:cd3906321824191cfd6972c1a8ad6f5171b49edc5996eba14bcbd33e98fb1ebe" + "sha256:6833ac65c6ed9711a4d5d2266f8024cfa07c533a0e55f4c12f6eff280a5a9e30", + "sha256:acea13255093849de7ccb11af9e1fb8bde7067783450cee9ef7a93139bddf6d4" ], "markers": "python_version >= '3.6'", - "version": "==6.4.0" + "version": "==6.4.2" }, "six": { "hashes": [ diff --git a/nbs/pybx_walkthrough.ipynb b/nbs/pybx_walkthrough_0.1.3.ipynb similarity index 100% rename from nbs/pybx_walkthrough.ipynb rename to nbs/pybx_walkthrough_0.1.3.ipynb diff --git a/src/pybx/basics.py b/src/pybx/basics.py index d623ea9..66ca69a 100644 --- a/src/pybx/basics.py +++ b/src/pybx/basics.py @@ -8,6 +8,7 @@ from .excepts import * __all__ = ['bbx', 'mbx', 'jbx', 'lbx', 'get_bx', + 'stack_bxs', 'add_bxs', 'BaseBx', 'MultiBx', 'JsonBx', 'ListBx'] @@ -98,14 +99,14 @@ def __add__(self, other): holds more than 1 coordinate/label for the box. From `v1.0.0`, a `UserWarning` is issued if called. Recommended use is either: `BaseBx` + `BaseBx` = `MultiBx` or - `ops.add_bxs()`. + `basics.stack_bxs()`. """ if not isinstance(other, (BaseBx, MultiBx, JsonBx, ListBx)): raise TypeError(f'{__name__}: Expected type MultiBx/JsonBx/ListBx') if isinstance(other, (BaseBx, MultiBx, JsonBx, ListBx)): warnings.warn(BxViolation(f'Change of object type imminent if trying to add ' f'{type(self)}+{type(other)}. Use {type(other)}+{type(self)} ' - f'instead or ops.add_bxs().')) + f'instead or basics.stack_bxs().')) coords = np.vstack([self.coords, other.coords]) label = self.label + other.label return mbx(coords, label) @@ -186,7 +187,7 @@ def __next__(self): def __add__(self, other): """Pseudo-add method that stacks the provided boxes and labels. Stacking two boxes imply that the resulting box is a `MultiBx`: `MultiBx` + `MultiBx` - = `MultiBx`. Same as `ops.add_bxs()`. + = `MultiBx`. Same as `basics.stack_bxs()`. """ if not isinstance(other, (BaseBx, MultiBx, JsonBx, ListBx)): raise TypeError(f'{__name__}: Expected type BaseBx/MultiBx/JsonBx/ListBx, ' @@ -298,6 +299,29 @@ def get_bx(coords, label=None): raise NotImplementedError(f'{__name__}: Got coords={coords} of type {type(coords)}.') +def stack_bxs(b1, b2): + """Method to stack two BxTypes together. Similar to `__add__` of BxTypes + but avoids UserWarning. + :param b1: Bx of class BaseBx, MultiBx, JsonBx, ListBx + :param b2: Bx of class BaseBx, MultiBx, JsonBx, ListBx + :return: MultiBx + """ + if not isinstance(b1, (BaseBx, MultiBx, JsonBx, ListBx)): + raise TypeError(f'{__name__}: Expected type BaseBx/MultiBx/JsonBx/ListBx, got b1={type(b1)}') + if not isinstance(b2, (BaseBx, MultiBx, JsonBx, ListBx)): + raise TypeError(f'{__name__}: Expected type BaseBx/MultiBx/JsonBx/ListBx, got b2={type(b2)}') + if isinstance(b1, BaseBx): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + return b1 + b2 + return b1 + b2 + + +def add_bxs(b1, b2): + """Alias of stack_bxs().""" + return stack_bxs(b1, b2) + + def jbx(coords=None, labels=None): """Abstraction of the JsonBx class to process `json` records into `MultiBx` or `BaseBx` objects exposing many validation methods @@ -313,12 +337,10 @@ def lbx(coords=None, labels=None): def bbx(coords=None, labels=None): - """Abstraction of the `BaseBx` class. - """ + """Abstraction of the `BaseBx` class.""" return BaseBx.basebx(coords, labels) def mbx(coords=None, labels=None): - """Abstraction of the `MultiBx` class. - """ + """Abstraction of the `MultiBx` class.""" return MultiBx.multibox(coords, labels) diff --git a/src/pybx/sample.py b/src/pybx/sample.py index 415b1b6..13188cb 100644 --- a/src/pybx/sample.py +++ b/src/pybx/sample.py @@ -38,7 +38,7 @@ def _get_scaled_annots(annots: list, new_sz: tuple, ann_im_sz=(300, 300, 3)): scaled = [] for annot in annots: d = {} - assert isinstance(annot, dict), f'{__name__}: Expected annots of type dict, got {type(annots)}' + assert isinstance(annot, dict), f'{__name__}: Expected annots of type dict, got {type(annot)}' for k, v in annot.items(): if k.startswith('x'): v_ = new_sz[0] * v / ann_im_sz[0] @@ -53,14 +53,14 @@ def _get_scaled_annots(annots: list, new_sz: tuple, ann_im_sz=(300, 300, 3)): def _get_example(image_sz: tuple = None, feature_sz: tuple = None, pth='.', img_fn='image.jpg', - load_ann=False, ann_fn='annots.json', logits=None, color: dict = {}): + load_ann=True, ann_fn='annots.json', logits=None, color: dict = {}): """Get an example image from the pth given for some image size for a feature size. :param image_sz: size to resize the loaded image a different size (annotations scaled automatically) :param feature_sz: Feature size to generate random logits if `logits` is not None. :param pth: path to find `ann_fn` and `img_fn`, default `.` :param img_fn: image file name, default `annots.json` :param load_ann: whether to load ann_fn or just the img_fn. - If False, an empty annotations dict is returned: `dict(zip(voc_keys, [0, 0, 1, 1, '']))` + If False, an empty annotations dict is returned: `[dict(zip(voc_keys, [0, 0, 1, 1, '']))]` :param ann_fn: annotations file name, default `image.jpg` :param logits: activations that should be overlayed from a neural network (no checks) :param color: A dict of `color` can be passed to assign specific color to a @@ -75,7 +75,7 @@ def _get_example(image_sz: tuple = None, feature_sz: tuple = None, pth='.', img_ if image_sz is not None: # reshaped image size image_arr = np.asarray(PIL.Image.fromarray(image_arr).resize(list(image_sz[:2]))) - annots = dict(zip(voc_keys, [0, 0, 1, 1, ''])) # default values + annots = [dict(zip(voc_keys, [0, 0, 1, 1, '']))] # default values if load_ann: assert ann_fn is not None, f'{__name__}: got ann_fn={ann_fn} with show_ann={load_ann}' assert os.path.exists(os.path.join(pth, ann_fn)), f'{pth} has no {ann_fn}' @@ -83,6 +83,8 @@ def _get_example(image_sz: tuple = None, feature_sz: tuple = None, pth='.', img_ annots = json.load(f) # annots for 300x300 image if not np.all(ann_im_sz == image_sz): annots = _get_scaled_annots(annots, image_sz, ann_im_sz=ann_im_sz) + assert isinstance(annots, list), f'{__name__}: Expected annots should be list of list/dict, ' \ + f'got {annots} of type {type(annots)}' if logits is not None: # if ndarray/detached-tensor, use logits values if not hasattr(logits, 'shape'): @@ -109,7 +111,7 @@ def _get_given_array(image_arr: np.ndarray = None, annots: list = None, image_sz specific `label` in the image: `color = {'frame': 'blue', 'clock': 'green'}` :returns: image_arr, annots, logits, color """ - image_sz = (100, 100, 1) if image_sz is None else image_sz + image_sz = (100, 100, 3) if image_sz is None else image_sz image_arr = np.random.randint(size=image_sz, low=0, high=255) if image_arr is None else image_arr if logits is not None: # if ndarray/detached-tensor, use logits values diff --git a/src/pybx/version.py b/src/pybx/version.py index 1a72d32..c7c3b12 100644 --- a/src/pybx/version.py +++ b/src/pybx/version.py @@ -1 +1,2 @@ -__version__ = '1.1.0' +__version__ = '1.2.1' + diff --git a/src/pybx/vis.py b/src/pybx/vis.py index a696379..e32709a 100644 --- a/src/pybx/vis.py +++ b/src/pybx/vis.py @@ -52,7 +52,7 @@ def __init__(self, image_arr=None, image_sz=None, sample=False, **kwargs): ann = get_bx(ann) store_attr('im, ann, lgt, clr') - def show(self, coords, labels=None, color=None, ax=None): + def show(self, coords, labels=None, color=None, ax=None, **kwargs): """Calling the `show()` method of the `VisBx()` instance accepts bounding box coordinates and labels that are to be shown. The boxes can be provided as any of the internal objects (`MultiBx`, `BaseBx`, ...) @@ -61,7 +61,7 @@ def show(self, coords, labels=None, color=None, ax=None): if color is not None: self.clr.update(color) coords = get_bx(coords, labels) - return draw(self.im, self.ann + coords, color=self.clr, logits=self.lgt, ax=ax) + return draw(self.im, self.ann + coords, color=self.clr, logits=self.lgt, ax=ax, **kwargs) def draw(img: np.ndarray, bbox: list, logits=None, alpha=0.4, **kwargs): @@ -141,7 +141,7 @@ def get_extents(shape): return extent -def draw_boxes(img: np.ndarray, bbox: list, title=None, ax=None, figsize=(10, 8), +def draw_boxes(img: np.ndarray, bbox: list, title=None, ax=None, figsize=(5, 4), squeeze=False, color='yellow', no_ticks=False, xo=0, yo=0, **kwargs): """Method to draw bounding boxes in an image, can handle multiple bboxes. :param figsize: sige of figure diff --git a/tests/test_basics.py b/tests/test_basics.py index 94f0b45..fa14393 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -3,7 +3,7 @@ import numpy as np -from pybx.basics import mbx, bbx, MultiBx, jbx +from pybx.basics import mbx, bbx, MultiBx, jbx, stack_bxs from pybx.excepts import BxViolation np.random.seed(1) @@ -84,6 +84,15 @@ def test_add_warning(self): b1 = bbx(annots[1]) self.assertWarns(BxViolation, b0.__add__, other=b1) + def test_stack_bxs(self): + with open(params["annots_iou_file"]) as f: + annots = json.load(f) + b0 = bbx(annots[0]) + b1 = bbx(annots[1]) + bm = mbx(annots[:2]) + bs = stack_bxs(b0, b1) + self.assertTrue((bs.coords == bm.coords).all()) + def test_iou(self): with open(params["annots_iou_file"]) as f: annots = json.load(f) diff --git a/tests/test_vis.py b/tests/test_vis.py index 4412b9a..ba24394 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -11,6 +11,7 @@ params = { "data_dir": '../data', + "annots_file": 'annots_iou.json', "annots_iou_file": '../data/annots_iou.json', "annots_rand_file": '../data/annots_rand.json', "annots_l": [[50., 70., 120., 100., 'rand1'], [150., 200., 250., 240., 'rand2']], @@ -18,41 +19,45 @@ "annots_nd": np.random.randint(low=1, high=10, size=(2, 4)), "annots_json": [{'label': '', 'x_max': 0, 'x_min': 0, 'y_max': 0, 'y_min': 0}], "feature_sz": (2, 2), - "image_sz": (10, 10, 1), - "image_arr": np.random.randint(size=(10, 10, 1), low=0, high=255) + "image_sz": (10, 10, 3), + "image_arr": np.random.randint(size=(10, 10, 3), low=0, high=255) } class VisTestCase(unittest.TestCase): def __init__(self, args): super(VisTestCase, self).__init__(args) - # use image paths to load image + # use image paths to load image and anns self.v1 = VisBx(image_sz=params["image_sz"], feature_sz=params["feature_sz"], - logits=True, pth=params["data_dir"], sample=True, load_ann=True) + logits=True, pth=params["data_dir"], ann_fn=params["annots_file"], sample=True, load_ann=True) + + # use image paths to load image only dont load anns + self.v2 = VisBx(image_sz=params["image_sz"], pth=params["data_dir"], sample=True, load_ann=False) + # use image array directly with annots - self.v2 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"], feature_sz=params["feature_sz"]) + self.v3 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"], feature_sz=params["feature_sz"]) # use image array directly with 1D annots - self.v3 = VisBx(image_arr=params["image_arr"], annots=params["annots_1d"], feature_sz=params["feature_sz"]) + self.v4 = VisBx(image_arr=params["image_arr"], annots=params["annots_1d"], feature_sz=params["feature_sz"]) # use image array directly with ND annots - self.v4 = VisBx(image_arr=params["image_arr"], annots=params["annots_nd"], feature_sz=params["feature_sz"]) + self.v5 = VisBx(image_arr=params["image_arr"], annots=params["annots_nd"], feature_sz=params["feature_sz"]) # use random image array - self.v5 = VisBx() + self.v6 = VisBx() # use logits data with image array - self.v6 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"], feature_sz=params["feature_sz"], + self.v7 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"], feature_sz=params["feature_sz"], logits=np.random.randn(*params["feature_sz"])) # use logits data with image array but single anns - self.v7 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"][0], feature_sz=params["feature_sz"], + self.v8 = VisBx(image_arr=params["image_arr"], annots=params["annots_l"][0], feature_sz=params["feature_sz"], logits=np.random.randn(*params["feature_sz"])) # use annots json - self.v8 = VisBx(image_arr=params["image_arr"], annots=params["annots_json"], feature_sz=params["feature_sz"]) + self.v9 = VisBx(image_arr=params["image_arr"], annots=params["annots_json"], feature_sz=params["feature_sz"]) - self.vs = [self.v1, self.v2, self.v3, self.v4, self.v5, self.v6, self.v7, self.v8] + self.vs = [self.v1, self.v2, self.v3, self.v4, self.v5, self.v6, self.v7, self.v8, self.v9] def test_vis_bx(self): with open(params["annots_rand_file"]) as f: