Skip to content

Commit

Permalink
Patch 5: Minor fixes (#5)
Browse files Browse the repository at this point in the history
- Fixed `load_ann=True` 
- return default `anns` in `get_example` as json
- added `stack_bxs` method
  • Loading branch information
thatgeeman authored Jan 20, 2022
1 parent 0ee38c6 commit d4f7758
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 39 deletions.
4 changes: 0 additions & 4 deletions .idea/misc.xml

This file was deleted.

2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 5 additions & 5 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
36 changes: 29 additions & 7 deletions src/pybx/basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .excepts import *

__all__ = ['bbx', 'mbx', 'jbx', 'lbx', 'get_bx',
'stack_bxs', 'add_bxs',
'BaseBx', 'MultiBx', 'JsonBx', 'ListBx']


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, '
Expand Down Expand Up @@ -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
Expand All @@ -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)
12 changes: 7 additions & 5 deletions src/pybx/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -75,14 +75,16 @@ 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}'
with open(os.path.join(pth, ann_fn)) as f:
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'):
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/pybx/version.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__version__ = '1.1.0'
__version__ = '1.2.1'

6 changes: 3 additions & 3 deletions src/pybx/vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`, ...)
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 17 additions & 12 deletions tests/test_vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,53 @@

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']],
"annots_1d": np.random.randint(low=1, high=10, size=4),
"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:
Expand Down

0 comments on commit d4f7758

Please sign in to comment.