From da5ad7109503a765527417684ef1a6c5f05ed475 Mon Sep 17 00:00:00 2001 From: kunkunlin1221 Date: Fri, 29 Aug 2025 10:53:51 +0800 Subject: [PATCH] [F] Fix jsonable --- .gitignore | 3 +- capybara/__init__.py | 2 +- capybara/mixins.py | 36 +++------ capybara/structures/keypoints.py | 81 ++++++++++--------- tests/test_mixins.py | 133 ++++++++++++++++++++++++------- 5 files changed, 161 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index d5df6de..79f632a 100644 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,5 @@ temp_image.jpg # .DS_Store -.python-version \ No newline at end of file +.python-version +tmp.jpg diff --git a/capybara/__init__.py b/capybara/__init__.py index 573f783..0574368 100644 --- a/capybara/__init__.py +++ b/capybara/__init__.py @@ -5,4 +5,4 @@ from .utils import * from .vision import * -__version__ = '0.11.0' +__version__ = "0.11.0" diff --git a/capybara/mixins.py b/capybara/mixins.py index 4576311..6116d61 100644 --- a/capybara/mixins.py +++ b/capybara/mixins.py @@ -8,11 +8,13 @@ import numpy as np from dacite import from_dict -from .structures import Box, Boxes, Polygon, Polygons +from .structures import Box, Boxes, Keypoints, KeypointsList, Polygon, Polygons __all__ = [ - 'EnumCheckMixin', 'DataclassCopyMixin', 'DataclassToJsonMixin', - 'dict_to_jsonable', + "EnumCheckMixin", + "DataclassCopyMixin", + "DataclassToJsonMixin", + "dict_to_jsonable", ] @@ -27,18 +29,14 @@ def dict_to_jsonable( out[k] = jsonable_func[k](v) else: if isinstance(v, (Box, Boxes)): - out[k] = v.convert('XYXY').numpy().astype( - float).round().tolist() - elif isinstance(v, (Polygon, Polygons)): + out[k] = v.convert("XYXY").numpy().astype(float).round().tolist() + elif isinstance(v, (Keypoints, KeypointsList, Polygon, Polygons)): out[k] = v.numpy().astype(float).round().tolist() elif isinstance(v, (np.ndarray, np.generic)): + # include array and scalar, if you want jsonable image please use jsonable_func out[k] = v.tolist() elif isinstance(v, (list, tuple)): - out[k] = [ - dict_to_jsonable(x, jsonable_func) if isinstance( - x, dict) else x - for x in v - ] + out[k] = [dict_to_jsonable(x, jsonable_func) if isinstance(x, dict) else x for x in v] elif isinstance(v, Enum): out[k] = v.name elif isinstance(v, Mapping): @@ -55,7 +53,6 @@ def dict_to_jsonable( class EnumCheckMixin: - @classmethod def obj_to_enum(cls: Enum, obj: Any): if isinstance(obj, str): @@ -75,12 +72,8 @@ def obj_to_enum(cls: Enum, obj: Any): class DataclassCopyMixin: - def __copy__(self): - return self.__class__(**{ - field: getattr(self, field) - for field in self.__dataclass_fields__ - }) + return self.__class__(**{field: getattr(self, field) for field in self.__dataclass_fields__}) def __deepcopy__(self, memo): out = asdict(self, dict_factory=OrderedDict) @@ -88,13 +81,8 @@ def __deepcopy__(self, memo): class DataclassToJsonMixin: - - def __init__(self): - self.jsonable_func = None + jsonable_func = None def be_jsonable(self, dict_factory=OrderedDict): d = asdict(self, dict_factory=dict_factory) - return dict_to_jsonable(d, getattr(self, 'jsonable_func', None), dict_factory) - - def regist_jsonable_func(self, jsonable_func: Optional[Dict[str, Callable]] = None): - self.jsonable_func = jsonable_func + return dict_to_jsonable(d, jsonable_func=self.jsonable_func, dict_factory=dict_factory) diff --git a/capybara/structures/keypoints.py b/capybara/structures/keypoints.py index 807b17c..ae8e5d9 100644 --- a/capybara/structures/keypoints.py +++ b/capybara/structures/keypoints.py @@ -5,9 +5,8 @@ import numpy as np from ..typing import _Number -from .boxes import Box, Boxes -__all__ = ['Keypoints', 'KeypointsList'] +__all__ = ["Keypoints", "KeypointsList"] _Keypoints = Union[ @@ -25,17 +24,17 @@ class Keypoints: - ''' + """ This structure has shape (K, 3) or (K, 2) where K is the number of keypoints. The visibility flag follows the COCO format and must be one of three integers: * v=0: not labeled (in which case x=y=0) * v=1: labeled but not visible * v=2: labeled and visible - ''' + """ - def __init__(self, array: _Keypoints, cmap='rainbow', is_normalized: bool = False): + def __init__(self, array: _Keypoints, cmap="rainbow", is_normalized: bool = False): self._array = self._check_valid_array(array) - steps = np.linspace(0., 1., self._array.shape[-2]) + steps = np.linspace(0.0, 1.0, self._array.shape[-2]) color_map = matplotlib.colormaps[cmap] self._point_colors = np.array(color_map(steps, bytes=True))[..., :3].tolist() self._is_normalized = is_normalized @@ -62,16 +61,16 @@ def _check_valid_array(self, array: Any) -> np.ndarray: if cond3: array = array.numpy() else: - array = np.array(array, dtype='float32') + array = np.array(array, dtype="float32") if not array.ndim == 2: raise ValueError(f"Input array ndim = {array.ndim} is not 2, which is invalid.") - if not array.shape[-1] in [2, 3]: + if array.shape[-1] not in [2, 3]: raise ValueError(f"Input array's shape[-1] = {array.shape[-1]} is not in [2, 3], which is invalid.") if array.shape[-1] == 3 and not ((array[..., 2] <= 2).all() and (array[..., 2] >= 0).all()): - raise ValueError('Given array is invalid because of its labels. (array[..., 2])') + raise ValueError("Given array is invalid because of its labels. (array[..., 2])") return array.copy() def numpy(self) -> np.ndarray: @@ -92,7 +91,7 @@ def scale(self, fx: float, fy: float) -> "Keypoints": def normalize(self, w: float, h: float) -> "Keypoints": if self.is_normalized: - warn(f'Normalized keypoints are forced to do normalization.') + warn("Normalized keypoints are forced to do normalization.") arr = self._array.copy() arr[..., :2] = arr[..., :2] / (w, h) kpts = self.__class__(arr) @@ -101,37 +100,33 @@ def normalize(self, w: float, h: float) -> "Keypoints": def denormalize(self, w: float, h: float) -> "Keypoints": if not self.is_normalized: - warn(f'Non-normalized keypoints is forced to do denormalization.') + warn("Non-normalized keypoints is forced to do denormalization.") arr = self._array.copy() arr[..., :2] = arr[..., :2] * (w, h) kpts = self.__class__(arr) kpts._is_normalized = False return kpts - @ property + @property def is_normalized(self) -> bool: return self._is_normalized - @ property + @property def point_colors(self) -> List[Tuple[int, int, int]]: - return [ - tuple([int(x) for x in cs]) - for cs in self._point_colors - ] + return [tuple([int(x) for x in cs]) for cs in self._point_colors] - @ point_colors.setter + @point_colors.setter def set_point_colors(self, cmap: str): - steps = np.linspace(0., 1., self._array.shape[-2]) + steps = np.linspace(0.0, 1.0, self._array.shape[-2]) self._point_colors = matplotlib.colormaps[cmap](steps, bytes=True) class KeypointsList: - - def __init__(self, array: _KeypointsList, cmap='rainbow', is_normalized: bool = False) -> None: + def __init__(self, array: _KeypointsList, cmap="rainbow", is_normalized: bool = False) -> None: self._array = self._check_valid_array(array).copy() self._is_normalized = is_normalized if len(self._array): - steps = np.linspace(0., 1., self._array.shape[-2]) + steps = np.linspace(0.0, 1.0, self._array.shape[-2]) self._point_colors = matplotlib.colormaps[cmap](steps, bytes=True) else: self._point_colors = None @@ -146,7 +141,7 @@ def __getitem__(self, item) -> Any: def __setitem__(self, item, value): if not isinstance(value, (Keypoints, KeypointsList)): - raise TypeError(f'Input value is not a keypoint or keypoints') + raise TypeError("Input value is not a keypoint or keypoints") if isinstance(item, (int, np.ndarray, list, slice)): self._array[item] = value._array @@ -166,9 +161,13 @@ def __eq__(self, value: object) -> bool: def _check_valid_array(self, array: Any) -> np.ndarray: cond1 = isinstance(array, np.ndarray) cond2 = isinstance(array, list) and len(array) == 0 - cond3 = isinstance(array, list) and \ - all(isinstance(x, (np.ndarray, Keypoints)) for x in array) or \ - all(isinstance(y, tuple) for x in array for y in x) + cond3 = ( + isinstance(array, list) + and ( + all(isinstance(x, (np.ndarray, Keypoints)) for x in array) + or all(isinstance(y, tuple) for x in array for y in x) + ) + ) cond4 = isinstance(array, self.__class__) if not (cond1 or cond2 or cond3 or cond4): @@ -177,9 +176,9 @@ def _check_valid_array(self, array: Any) -> np.ndarray: if cond4: array = array.numpy() elif len(array) and isinstance(array[0], Keypoints): - array = np.array([x.numpy() for x in array], dtype='float32') + array = np.array([x.numpy() for x in array], dtype="float32") else: - array = np.array(array, dtype='float32') + array = np.array(array, dtype="float32") if len(array) == 0: return array @@ -191,7 +190,7 @@ def _check_valid_array(self, array: Any) -> np.ndarray: raise ValueError(f"Input array's shape[-1] = {array.shape[-1]} is not 2 or 3, which is invalid.") if array.shape[-1] == 3 and not ((array[..., 2] <= 2).all() and (array[..., 2] >= 0).all()): - raise ValueError('Given array is invalid because of its labels. (array[..., 2])') + raise ValueError("Given array is invalid because of its labels. (array[..., 2])") return array @@ -213,7 +212,7 @@ def scale(self, fx: float, fy: float) -> Any: def normalize(self, w: float, h: float) -> "KeypointsList": if self.is_normalized: - warn(f'Normalized keypoints_list is forced to do normalization.') + warn("Normalized keypoints_list is forced to do normalization.") arr = self._array.copy() arr[..., :2] = arr[..., :2] / (w, h) kpts_list = self.__class__(arr) @@ -222,29 +221,29 @@ def normalize(self, w: float, h: float) -> "KeypointsList": def denormalize(self, w: float, h: float) -> "KeypointsList": if not self.is_normalized: - warn(f'Non-normalized box is forced to do denormalization.') + warn("Non-normalized box is forced to do denormalization.") arr = self._array.copy() arr[..., :2] = arr[..., :2] * (w, h) kpts_list = self.__class__(arr) kpts_list._is_normalized = False return kpts_list - @ property + @property def is_normalized(self) -> bool: return self._is_normalized - @ property + @property def point_colors(self): return [tuple(c) for c in self._point_colors[..., :3].tolist()] - @ point_colors.setter + @point_colors.setter def set_point_colors(self, cmap: str): - steps = np.linspace(0., 1., self._array.shape[-2]) + steps = np.linspace(0.0, 1.0, self._array.shape[-2]) self._point_colors = matplotlib.colormaps[cmap](steps, bytes=True) - @ classmethod + @classmethod def cat(cls, keypoints_lists: List["KeypointsList"]) -> "KeypointsList": - ''' + """ Concatenates a list of KeypointsList into a single KeypointsList Raises: @@ -254,14 +253,14 @@ def cat(cls, keypoints_lists: List["KeypointsList"]) -> "KeypointsList": Returns: Keypoints: the concatenated Keypoints - ''' + """ if not isinstance(keypoints_lists, list): - raise TypeError('Given keypoints_list should be a list.') + raise TypeError("Given keypoints_list should be a list.") if len(keypoints_lists) == 0: - raise ValueError('Given keypoints_list is empty.') + raise ValueError("Given keypoints_list is empty.") if not all(isinstance(keypoints_list, KeypointsList) for keypoints_list in keypoints_lists): - raise TypeError('All type of elements in keypoints_lists must be KeypointsList.') + raise TypeError("All type of elements in keypoints_lists must be KeypointsList.") return cls(np.concatenate([keypoints_list.numpy() for keypoints_list in keypoints_lists], axis=0)) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index bb10f92..062055c 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -1,43 +1,46 @@ from copy import deepcopy -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum -from typing import Any, List +from typing import Any, Callable, Dict, List import numpy as np import pytest -import capybara as D -from capybara import (DataclassCopyMixin, DataclassToJsonMixin, EnumCheckMixin, - dict_to_jsonable) +import capybara as cb +from capybara import DataclassCopyMixin, DataclassToJsonMixin, EnumCheckMixin, dict_to_jsonable -MockImage = np.zeros((5, 5, 3), dtype='uint8') -base64png_Image = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAADElEQVQIHWNgoC4AAABQAAFhFZyBAAAAAElFTkSuQmCC' -base64npy_Image = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +MockImage = np.zeros((5, 5, 3), dtype="uint8") +base64png_Image = "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAADElEQVQIHWNgoC4AAABQAAFhFZyBAAAAAElFTkSuQmCC" +base64npy_Image = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" data = [ ( dict( - box=D.Box((0, 0, 1, 1)), - boxes=D.Boxes([(0, 0, 1, 1)]), - polygon=D.Polygon([(0, 0), (1, 0), (1, 1)]), - polygons=D.Polygons([[(0, 0), (1, 0), (1, 1)]]), + box=cb.Box((0, 0, 1, 1)), + boxes=cb.Boxes([(0, 0, 1, 1)]), + keypoints=cb.Keypoints([(0, 1), (1, 0)]), + keypoints_list=cb.KeypointsList([[(0, 1), (1, 0)], [(0, 1), (2, 0)]]), + polygon=cb.Polygon([(0, 0), (1, 0), (1, 1)]), + polygons=cb.Polygons([[(0, 0), (1, 0), (1, 1)]]), np_bool=np.bool_(True), np_float=np.float64(1), np_number=np.array(1), np_array=np.array([1, 2]), image=MockImage, - dict=dict(box=D.Box((0, 0, 1, 1))), - str='test', + dict=dict(box=cb.Box((0, 0, 1, 1))), + str="test", int=1, float=0.6, tuple=(1, 1), pow=1e10, ), dict( - image=lambda x: D.img_to_b64str(x, D.IMGTYP.PNG), + image=lambda x: cb.img_to_b64str(x, cb.IMGTYP.PNG), ), dict( box=[0, 0, 1, 1], boxes=[[0, 0, 1, 1]], + keypoints=[[0, 1], [1, 0]], + keypoints_list=[[[0, 1], [1, 0]], [[0, 1], [2, 0]]], polygon=[[0, 0], [1, 0], [1, 1]], polygons=[[[0, 0], [1, 0], [1, 1]]], np_bool=True, @@ -46,39 +49,39 @@ np_array=[1, 2], image=base64png_Image, dict=dict(box=[0, 0, 1, 1]), - str='test', + str="test", int=1, float=0.6, tuple=[1, 1], pow=1e10, - ) + ), ), ( dict( image=MockImage, ), dict( - image=lambda x: D.npy_to_b64str(x), + image=lambda x: cb.npy_to_b64str(x), ), dict( image=base64npy_Image, - ) + ), ), ( dict( images=[dict(image=MockImage)], ), dict( - image=lambda x: D.npy_to_b64str(x), + image=lambda x: cb.npy_to_b64str(x), ), dict( images=[dict(image=base64npy_Image)], - ) + ), ), ] -@pytest.mark.parametrize('x,jsonable_func,expected', data) +@pytest.mark.parametrize("x,jsonable_func,expected", data) def test_dict_to_jsonable(x, jsonable_func, expected): assert dict_to_jsonable(x, jsonable_func) == expected @@ -89,7 +92,6 @@ class TestEnum(EnumCheckMixin, Enum): class TestEnumCheckMixin: - def test_obj_to_enum_with_valid_enum_member(self): assert TestEnum.obj_to_enum(TestEnum.FIRST) == TestEnum.FIRST @@ -115,7 +117,6 @@ class TestDataclass(DataclassCopyMixin): class TestDataclassCopyMixin: - @pytest.fixture def test_dataclass_instance(self): return TestDataclass(10, [1, 2, 3]) @@ -135,6 +136,84 @@ def test_deep_copy(self, test_dataclass_instance): @dataclass -class TestDataclassJson(DataclassToJsonMixin, DataclassCopyMixin): - int_field: int - list_field: List[Any] +class TestDataclass2(DataclassToJsonMixin): + box: cb.Box + boxes: cb.Boxes + keypoints: cb.Keypoints + keypoints_list: cb.KeypointsList + polygon: cb.Polygon + polygons: cb.Polygons + np_bool: np.bool + np_float: np.float64 + np_number: np.ndarray + np_array: np.ndarray + np_array_to_b64str: np.ndarray + image: np.ndarray + py_dict: dict + py_str: str + py_int: int + py_float: float + py_tuple: tuple + py_pow: float + + # based on mixin + jsonable_func = { + "image": lambda x: cb.img_to_b64str(x, cb.IMGTYP.PNG), + "np_array_to_b64str": lambda x: cb.npy_to_b64str(x), + } + + +np_array_to_b64str = np.array([1.1, 2.1], dtype="float32") +np_array_to_b64str_b64 = "zcyMP2ZmBkA=" + + +class TestDataclassToJsonMixin: + @pytest.fixture + def test_dataclass_instance(self): + data = TestDataclass2( + box=cb.Box((0, 0, 1, 1)), + boxes=cb.Boxes([(0, 0, 1, 1)]), + keypoints=cb.Keypoints([(0, 1), (1, 0)]), + keypoints_list=cb.KeypointsList([[(0, 1), (1, 0)], [(0, 1), (2, 0)]]), + polygon=cb.Polygon([(0, 0), (1, 0), (1, 1)]), + polygons=cb.Polygons([[(0, 0), (1, 0), (1, 1)]]), + np_bool=np.bool_(True), + np_float=np.float64(1), + np_number=np.array(1), + np_array=np.array([1, 2]), + image=MockImage, + np_array_to_b64str=np_array_to_b64str, + py_dict=dict(box=cb.Box((0, 0, 1, 1))), + py_str="test", + py_int=1, + py_float=0.6, + py_tuple=(1, 1), + py_pow=1e10, + ) + return data + + @pytest.fixture + def test_expected(self): + return dict( + box=[0, 0, 1, 1], + boxes=[[0, 0, 1, 1]], + keypoints=[[0, 1], [1, 0]], + keypoints_list=[[[0, 1], [1, 0]], [[0, 1], [2, 0]]], + polygon=[[0, 0], [1, 0], [1, 1]], + polygons=[[[0, 0], [1, 0], [1, 1]]], + np_bool=True, + np_float=1.0, + np_number=1, + np_array=[1, 2], + image=base64png_Image, + np_array_to_b64str=np_array_to_b64str_b64, + py_dict=dict(box=[0, 0, 1, 1]), + py_str="test", + py_int=1, + py_float=0.6, + py_tuple=[1, 1], + py_pow=1e10, + ) + + def test_be_jsonable(self, test_dataclass_instance, test_expected): + assert test_dataclass_instance.be_jsonable() == test_expected