Skip to content

Commit 655f282

Browse files
Add HStack Layout (#250)
* Add HStack Layout Signed-off-by: Ashwin Vaidya <[email protected]> * Update copyright year in visualization modules to reflect 2024-2025 * Fix color assignment in HStack layout test: swap blue and red overlay colors --------- Signed-off-by: Ashwin Vaidya <[email protected]>
1 parent e5ba398 commit 655f282

File tree

7 files changed

+158
-29
lines changed

7 files changed

+158
-29
lines changed

model_api/python/model_api/visualizer/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
# Copyright (C) 2024 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

6-
from .layout import Flatten, Layout
6+
from .layout import Flatten, HStack, Layout
77
from .primitive import Overlay
88
from .scene import Scene
99
from .visualizer import Visualizer
1010

11-
__all__ = ["Overlay", "Scene", "Visualizer", "Layout", "Flatten"]
11+
__all__ = ["Overlay", "Scene", "Visualizer", "Layout", "Flatten", "HStack"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Visualization Layout."""
2+
3+
# Copyright (C) 2024-2025 Intel Corporation
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
from .flatten import Flatten
7+
from .hstack import HStack
8+
from .layout import Layout
9+
10+
__all__ = ["Flatten", "HStack", "Layout"]
Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
"""Visualization Layout"""
1+
"""Flatten Layout."""
22

3-
# Copyright (C) 2024 Intel Corporation
3+
# Copyright (C) 2024-2025 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

66
from __future__ import annotations
77

8-
from abc import ABC, abstractmethod
9-
from typing import TYPE_CHECKING, Type
8+
from typing import TYPE_CHECKING, Type, Union
9+
10+
from .layout import Layout
1011

1112
if TYPE_CHECKING:
1213
import PIL
1314

1415
from model_api.visualizer.primitive import Primitive
16+
from model_api.visualizer.scene import Scene
1517

16-
from .scene import Scene
1718

19+
class Flatten(Layout):
20+
"""Put all primitives on top of each other.
1821
19-
class Layout(ABC):
20-
"""Base class for layouts."""
22+
Args:
23+
*args (Union[Type[Primitive], Layout]): Primitives or layouts to be applied.
24+
"""
25+
26+
def __init__(self, *args: Union[Type[Primitive], Layout]) -> None:
27+
self.children = args
2128

2229
def _compute_on_primitive(self, primitive: Type[Primitive], image: PIL.Image, scene: Scene) -> PIL.Image | None:
2330
if scene.has_primitives(primitive):
@@ -27,25 +34,8 @@ def _compute_on_primitive(self, primitive: Type[Primitive], image: PIL.Image, sc
2734
return image
2835
return None
2936

30-
@abstractmethod
31-
def __call__(self, scene: Scene) -> PIL.Image:
32-
"""Compute the layout."""
33-
34-
35-
class Flatten(Layout):
36-
"""Put all primitives on top of each other.
37-
38-
Args:
39-
*args (Type[Primitive]): Primitives to be applied.
40-
"""
41-
42-
def __init__(self, *args: Type[Primitive]) -> None:
43-
self.children = args
44-
4537
def __call__(self, scene: Scene) -> PIL.Image:
4638
_image: PIL.Image = scene.base.copy()
4739
for child in self.children:
48-
output = self._compute_on_primitive(child, _image, scene)
49-
if output is not None:
50-
_image = output
40+
_image = child(scene) if isinstance(child, Layout) else self._compute_on_primitive(child, _image, scene)
5141
return _image
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Horizontal Stack Layout."""
2+
3+
# Copyright (C) 2024-2025 Intel Corporation
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING, Type, Union
9+
10+
import PIL
11+
12+
from .layout import Layout
13+
14+
if TYPE_CHECKING:
15+
from model_api.visualizer.primitive import Primitive
16+
from model_api.visualizer.scene import Scene
17+
18+
19+
class HStack(Layout):
20+
"""Horizontal Stack Layout.
21+
22+
Args:
23+
*args (Union[Type[Primitive], Layout]): Primitives or layouts to be applied.
24+
"""
25+
26+
def __init__(self, *args: Union[Type[Primitive], Layout]) -> None:
27+
self.children = args
28+
29+
def _compute_on_primitive(self, primitive: Type[Primitive], image: PIL.Image, scene: Scene) -> PIL.Image | None:
30+
if scene.has_primitives(primitive):
31+
images = []
32+
for _primitive in scene.get_primitives(primitive):
33+
_image = _primitive.compute(image.copy())
34+
images.append(_image)
35+
return self._stitch(*images)
36+
return None
37+
38+
@staticmethod
39+
def _stitch(*images: PIL.Image) -> PIL.Image:
40+
"""Stitch images horizontally.
41+
42+
Args:
43+
images (PIL.Image): Images to be stitched.
44+
45+
Returns:
46+
PIL.Image: Stitched image.
47+
"""
48+
new_image = PIL.Image.new(
49+
"RGB",
50+
(
51+
sum(image.width for image in images),
52+
max(image.height for image in images),
53+
),
54+
)
55+
x_offset = 0
56+
for image in images:
57+
new_image.paste(image, (x_offset, 0))
58+
x_offset += image.width
59+
return new_image
60+
61+
def __call__(self, scene: Scene) -> PIL.Image:
62+
"""Stitch images horizontally.
63+
64+
Args:
65+
scene (Scene): Scene to be stitched.
66+
67+
Returns:
68+
PIL.Image: Stitched image.
69+
"""
70+
images: list[PIL.Image] = []
71+
for child in self.children:
72+
if isinstance(child, Layout):
73+
_image = child(scene)
74+
else:
75+
_image = self._compute_on_primitive(child, scene.base.copy(), scene)
76+
if _image is not None:
77+
images.append(_image)
78+
return self._stitch(*images)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Visualization Layout."""
2+
3+
# Copyright (C) 2024-2025 Intel Corporation
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
from __future__ import annotations
7+
8+
from abc import ABC, abstractmethod
9+
from typing import TYPE_CHECKING, Type
10+
11+
if TYPE_CHECKING:
12+
import PIL
13+
14+
from model_api.visualizer.primitive import Primitive
15+
16+
from .scene import Scene
17+
18+
19+
class Layout(ABC):
20+
"""Base class for layouts."""
21+
22+
@abstractmethod
23+
def _compute_on_primitive(self, primitive: Type[Primitive], image: PIL.Image, scene: Scene) -> PIL.Image | None:
24+
pass
25+
26+
@abstractmethod
27+
def __call__(self, scene: Scene) -> PIL.Image:
28+
"""Compute the layout."""
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Visualization tests."""
22

3-
# Copyright (C) 2024 Intel Corporation
3+
# Copyright (C) 2024-2025 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0

tests/python/unit/visualizer/test_layout.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import numpy as np
77
from PIL import Image
88

9-
from model_api.visualizer import Flatten, Scene
9+
from model_api.visualizer import Flatten, HStack, Scene
1010
from model_api.visualizer.primitive import Overlay
1111

1212

@@ -25,3 +25,26 @@ def test_flatten_layout_with_no_primitives(mock_image: Image, mock_scene: Scene)
2525
"""Test if the layout is created correctly."""
2626
mock_scene.layout = Flatten()
2727
assert mock_scene.render() == mock_image
28+
29+
30+
def test_hstack_layout():
31+
"""Test if the layout is created correctly."""
32+
blue_overlay = np.zeros((100, 100, 3), dtype=np.uint8)
33+
blue_overlay[50, 50] = [0, 0, 255]
34+
blue_overlay = Image.fromarray(blue_overlay)
35+
36+
red_overlay = np.zeros((100, 100, 3), dtype=np.uint8)
37+
red_overlay[50, 50] = [255, 0, 0]
38+
red_overlay = Image.fromarray(red_overlay)
39+
40+
mock_scene = Scene(
41+
base=Image.new("RGB", (100, 100)),
42+
overlay=[Overlay(blue_overlay, opacity=1.0), Overlay(red_overlay, opacity=1.0)],
43+
layout=HStack(Overlay),
44+
)
45+
46+
expected_image = Image.new("RGB", (200, 100))
47+
expected_image.paste(blue_overlay, (0, 0))
48+
expected_image.paste(red_overlay, (100, 0))
49+
50+
assert mock_scene.render() == expected_image

0 commit comments

Comments
 (0)